add: cleaner ui, prior view check, audio element, rendering fixes
This commit is contained in:
parent
f215d038b3
commit
cacd992f53
7 changed files with 320 additions and 57 deletions
|
@ -147,6 +147,19 @@ globalThis.tab_editor = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
globalThis.get_preview = async () => {
|
||||||
|
return await (
|
||||||
|
await fetch("/api/v1/render", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: globalThis.editor.getValue(),
|
||||||
|
metadata: globalThis.metadata_editor.getValue(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
).text();
|
||||||
|
};
|
||||||
|
|
||||||
globalThis.tab_preview = async () => {
|
globalThis.tab_preview = async () => {
|
||||||
if (
|
if (
|
||||||
!document
|
!document
|
||||||
|
@ -157,16 +170,7 @@ globalThis.tab_preview = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// render
|
// render
|
||||||
const res = await (
|
const res = await get_preview();
|
||||||
await fetch("/api/v1/render", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
content: globalThis.editor.getValue(),
|
|
||||||
metadata: globalThis.metadata_editor.getValue(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
).text();
|
|
||||||
|
|
||||||
document.getElementById("preview_tab").innerHTML = res;
|
document.getElementById("preview_tab").innerHTML = res;
|
||||||
hljs.highlightAll();
|
hljs.highlightAll();
|
||||||
|
@ -366,3 +370,24 @@ setTimeout(() => {
|
||||||
// run initial hash check
|
// run initial hash check
|
||||||
hash_check(window.location.hash);
|
hash_check(window.location.hash);
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
|
globalThis.submitter_load = (submitter) => {
|
||||||
|
return {
|
||||||
|
load() {
|
||||||
|
submitter.querySelector("[ui_ident=text]").classList.add("hidden");
|
||||||
|
submitter
|
||||||
|
.querySelector("[ui_ident=loader]")
|
||||||
|
.classList.remove("hidden");
|
||||||
|
submitter.setAttribute("disabled", "true");
|
||||||
|
},
|
||||||
|
failed() {
|
||||||
|
submitter
|
||||||
|
.querySelector("[ui_ident=text]")
|
||||||
|
.classList.remove("hidden");
|
||||||
|
submitter
|
||||||
|
.querySelector("[ui_ident=loader]")
|
||||||
|
.classList.add("hidden");
|
||||||
|
submitter.removeAttribute("disabled");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
--color-green: hsl(100, 84%, 20%);
|
--color-green: hsl(100, 84%, 20%);
|
||||||
--color-yellow: oklch(47% 0.157 37.304);
|
--color-yellow: oklch(47% 0.157 37.304);
|
||||||
--color-purple: hsl(284, 84%, 20%);
|
--color-purple: hsl(284, 84%, 20%);
|
||||||
|
--color-green-lowered: hsl(100, 84%, 15%);
|
||||||
|
--color-red-lowered: hsl(0, 84%, 35%);
|
||||||
|
|
||||||
--shadow-x-offset: 0;
|
--shadow-x-offset: 0;
|
||||||
--shadow-y-offset: 0.125rem;
|
--shadow-y-offset: 0.125rem;
|
||||||
|
@ -207,6 +209,11 @@ video {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
opacity: 50%;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.button.small {
|
.button.small {
|
||||||
--h: 28px;
|
--h: 28px;
|
||||||
}
|
}
|
||||||
|
@ -243,6 +250,24 @@ video {
|
||||||
background: var(--color-super-raised);
|
background: var(--color-super-raised);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button.green:not(.dark *) {
|
||||||
|
background: var(--color-green);
|
||||||
|
color: white !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-green-lowered) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.red:not(.dark *) {
|
||||||
|
background: var(--color-red);
|
||||||
|
color: white !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-red-lowered) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* dropdown */
|
/* dropdown */
|
||||||
.dropdown {
|
.dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -272,9 +297,8 @@ video {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown .inner .title {
|
.dropdown .inner .title {
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
border-top: solid 1px var(--color-super-lowered);
|
font-size: 14px;
|
||||||
margin-top: var(--pad-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown:has(.inner.open) .button:nth-child(1):not(.inner *) {
|
.dropdown:has(.inner.open) .button:nth-child(1):not(.inner *) {
|
||||||
|
@ -710,6 +734,23 @@ span {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
animation: spin linear infinite 2s forwards running;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotateZ(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotateZ(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.items-end {
|
.items-end {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
@ -753,3 +794,32 @@ details .content {
|
||||||
padding: var(--pad-4);
|
padding: var(--pad-4);
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* dialog */
|
||||||
|
dialog {
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text);
|
||||||
|
box-shadow: var(--shadow-x-offset) var(--shadow-y-offset) var(--shadow-size)
|
||||||
|
var(--color-shadow);
|
||||||
|
animation: fadein ease-in-out 1 0.25s forwards running;
|
||||||
|
max-width: 95%;
|
||||||
|
width: 30rem;
|
||||||
|
margin: auto;
|
||||||
|
padding: var(--pad-4);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--pad-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog::backdrop {
|
||||||
|
background: hsla(0, 0%, 0%, 25%);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog:is(.dark *)::backdrop {
|
||||||
|
background: hsla(0, 0%, 100%, 15%);
|
||||||
|
}
|
||||||
|
|
|
@ -27,17 +27,19 @@
|
||||||
("title" "Info")
|
("title" "Info")
|
||||||
(text "i"))))
|
(text "i"))))
|
||||||
(div
|
(div
|
||||||
("class" "card tab tabs container")
|
("class" "flex justify_center tab")
|
||||||
("id" "tabs_group")
|
|
||||||
(div
|
(div
|
||||||
("id" "editor_tab")
|
("class" "card tab tabs container w_full")
|
||||||
("class" "tab fadein"))
|
("id" "tabs_group")
|
||||||
(div
|
(div
|
||||||
("id" "preview_tab")
|
("id" "editor_tab")
|
||||||
("class" "tab fadein hidden"))
|
("class" "tab fadein w_full"))
|
||||||
(div
|
(div
|
||||||
("id" "metadata_tab")
|
("id" "preview_tab")
|
||||||
("class" "tab fadein hidden")))
|
("class" "tab fadein hidden w_full"))
|
||||||
|
(div
|
||||||
|
("id" "metadata_tab")
|
||||||
|
("class" "tab fadein hidden w_full"))))
|
||||||
(form
|
(form
|
||||||
("class" "w_full flex flex_col gap_2")
|
("class" "w_full flex flex_col gap_2")
|
||||||
("style" "margin-top: var(--pad-2)")
|
("style" "margin-top: var(--pad-2)")
|
||||||
|
@ -80,7 +82,8 @@
|
||||||
("class" "flex gap_2")
|
("class" "flex gap_2")
|
||||||
(button
|
(button
|
||||||
("class" "button green")
|
("class" "button green")
|
||||||
(text "Save"))
|
(span ("ui_ident" "text") (text "Save"))
|
||||||
|
(span ("class" "hidden loader no_fill") ("ui_ident" "loader") (text "{{ icon \"loader-circle\" }}")))
|
||||||
(a
|
(a
|
||||||
("href" "/{{ entry.slug }}")
|
("href" "/{{ entry.slug }}")
|
||||||
("class" "button")
|
("class" "button")
|
||||||
|
@ -88,12 +91,37 @@
|
||||||
|
|
||||||
(button
|
(button
|
||||||
("class" "button red")
|
("class" "button red")
|
||||||
("ui_ident" "delete")
|
("type" "button")
|
||||||
(text "Delete"))))
|
("onclick" "document.getElementById('delete_modal').showModal()")
|
||||||
|
("id" "fake_delete_button")
|
||||||
|
(span ("ui_ident" "text") (text "Delete"))
|
||||||
|
(span ("class" "hidden loader no_fill") ("ui_ident" "loader") (text "{{ icon \"loader-circle\" }}")))
|
||||||
|
|
||||||
|
(dialog
|
||||||
|
("id" "delete_modal")
|
||||||
|
(div
|
||||||
|
("class" "inner")
|
||||||
|
(h2 ("class" "text_center w_full") (text "Delete {{ entry.slug }}?"))
|
||||||
|
(p (text "Deleting this entry will make its custom slug claimable by anyone."))
|
||||||
|
(p (text "Please ensure that you understand the consequences of deleting this entry before continuing."))
|
||||||
|
(hr ("class" "margin"))
|
||||||
|
(div
|
||||||
|
("class" "w_full flex gap_2 justify_between")
|
||||||
|
(button
|
||||||
|
("class" "button")
|
||||||
|
("type" "button")
|
||||||
|
("onclick" "document.getElementById('delete_modal').close()")
|
||||||
|
(text "Cancel"))
|
||||||
|
(button
|
||||||
|
("class" "button red")
|
||||||
|
("ui_ident" "delete")
|
||||||
|
("onclick" "document.getElementById('delete_modal').close()")
|
||||||
|
(text "Delete")))))))
|
||||||
|
|
||||||
; editor
|
; editor
|
||||||
(script ("src" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.js"))
|
(script ("src" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.js"))
|
||||||
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/markdown/markdown.js"))
|
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/markdown/markdown.js"))
|
||||||
|
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/toml/toml.js"))
|
||||||
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/display/placeholder.js"))
|
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/display/placeholder.js"))
|
||||||
(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.css"))
|
(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.css"))
|
||||||
(script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"))
|
(script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"))
|
||||||
|
@ -105,16 +133,15 @@
|
||||||
(script
|
(script
|
||||||
(text "setTimeout(() => {
|
(text "setTimeout(() => {
|
||||||
globalThis.init_editor();
|
globalThis.init_editor();
|
||||||
globalThis.init_editor(\"metadata_editor\", \"plain\", \"metadata_tab\", \"editor_metadata_content\");
|
globalThis.init_editor(\"metadata_editor\", \"toml\", \"metadata_tab\", \"editor_metadata_content\");
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
globalThis.edit_entry = (e) => {
|
globalThis.edit_entry = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const rm = e.submitter.getAttribute(\"ui_ident\") === \"delete\";
|
const rm = e.submitter.getAttribute(\"ui_ident\") === \"delete\";
|
||||||
|
|
||||||
if (rm && !confirm(\"Are you sure you want to do this?\")) {
|
const { load, failed } = submitter_load(rm ? document.getElementById(\"fake_delete_button\") : e.submitter);
|
||||||
return;
|
load();
|
||||||
}
|
|
||||||
|
|
||||||
fetch(\"/api/v1/entries/{{ entry.slug }}\", {
|
fetch(\"/api/v1/entries/{{ entry.slug }}\", {
|
||||||
method: \"POST\",
|
method: \"POST\",
|
||||||
|
@ -147,7 +174,36 @@
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
show_message(res.message, false);
|
show_message(res.message, false);
|
||||||
|
failed();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.download = (content, type, name) => {
|
||||||
|
const blob = new Blob([content], { type });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const anchor = document.createElement(\"a\");
|
||||||
|
|
||||||
|
anchor.setAttribute(\"download\", name);
|
||||||
|
anchor.href = url;
|
||||||
|
anchor.click();
|
||||||
|
anchor.remove();
|
||||||
}"))
|
}"))
|
||||||
(text "{% endblock %}")
|
(text "{% endblock %}")
|
||||||
|
|
||||||
|
(text "{% block dropdown %}")
|
||||||
|
(hr)
|
||||||
|
(span ("class" "title") (text "export"))
|
||||||
|
(button
|
||||||
|
("class" "button")
|
||||||
|
("onclick" "download(globalThis.editor.getValue(), 'text/markdown', '{{ entry.slug }}.md')")
|
||||||
|
(text "markdown"))
|
||||||
|
(button
|
||||||
|
("class" "button")
|
||||||
|
("onclick" "download(globalThis.metadata_editor.getValue(), 'application/toml', '{{ entry.slug }}.toml')")
|
||||||
|
(text "metadata"))
|
||||||
|
(button
|
||||||
|
("class" "button")
|
||||||
|
("onclick" "(async () => { download(await get_preview(), 'text/html', '{{ entry.slug }}.html') })();")
|
||||||
|
(text "html"))
|
||||||
|
(text "{%- endblock %}")
|
||||||
|
|
|
@ -30,24 +30,27 @@
|
||||||
("title" "Info")
|
("title" "Info")
|
||||||
(text "i"))))
|
(text "i"))))
|
||||||
(div
|
(div
|
||||||
("class" "card tab tabs container")
|
("class" "flex justify_center tab")
|
||||||
("id" "tabs_group")
|
|
||||||
(div
|
(div
|
||||||
("id" "editor_tab")
|
("class" "card tab tabs container w_full")
|
||||||
("class" "tab fadein"))
|
("id" "tabs_group")
|
||||||
(div
|
(div
|
||||||
("id" "preview_tab")
|
("id" "editor_tab")
|
||||||
("class" "tab fadein hidden"))
|
("class" "tab fadein w_full"))
|
||||||
(div
|
(div
|
||||||
("id" "metadata_tab")
|
("id" "preview_tab")
|
||||||
("class" "tab fadein hidden")))
|
("class" "tab fadein hidden w_full"))
|
||||||
|
(div
|
||||||
|
("id" "metadata_tab")
|
||||||
|
("class" "tab fadein hidden w_full"))))
|
||||||
(form
|
(form
|
||||||
("class" "w_full flex justify_between gap_2 flex_collapse_rev")
|
("class" "w_full flex justify_between gap_2 flex_collapse_rev")
|
||||||
("style" "margin-top: var(--pad-2)")
|
("style" "margin-top: var(--pad-2)")
|
||||||
("onsubmit" "create_entry(event)")
|
("onsubmit" "create_entry(event)")
|
||||||
(button
|
(button
|
||||||
("class" "button")
|
("class" "button")
|
||||||
(text "Go"))
|
(span ("ui_ident" "text") (text "Go"))
|
||||||
|
(span ("class" "hidden loader no_fill") ("ui_ident" "loader") (text "{{ icon \"loader-circle\" }}")))
|
||||||
(div
|
(div
|
||||||
("class" "flex gap_2")
|
("class" "flex gap_2")
|
||||||
(input
|
(input
|
||||||
|
@ -68,6 +71,7 @@
|
||||||
; editor
|
; editor
|
||||||
(script ("src" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.js"))
|
(script ("src" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.js"))
|
||||||
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/markdown/markdown.js"))
|
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/markdown/markdown.js"))
|
||||||
|
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/toml/toml.js"))
|
||||||
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/display/placeholder.js"))
|
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/display/placeholder.js"))
|
||||||
(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.css"))
|
(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.css"))
|
||||||
(script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"))
|
(script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"))
|
||||||
|
@ -76,11 +80,15 @@
|
||||||
(script
|
(script
|
||||||
(text "setTimeout(() => {
|
(text "setTimeout(() => {
|
||||||
globalThis.init_editor();
|
globalThis.init_editor();
|
||||||
globalThis.init_editor(\"metadata_editor\", \"plain\", \"metadata_tab\");
|
globalThis.init_editor(\"metadata_editor\", \"toml\", \"metadata_tab\");
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
globalThis.create_entry = (e) => {
|
globalThis.create_entry = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
const { load, failed } = submitter_load(e.submitter);
|
||||||
|
load();
|
||||||
|
|
||||||
fetch(\"/api/v1/entries\", {
|
fetch(\"/api/v1/entries\", {
|
||||||
method: \"POST\",
|
method: \"POST\",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -102,6 +110,7 @@
|
||||||
window.location.href = `/${res.payload[0]}`;
|
window.location.href = `/${res.payload[0]}`;
|
||||||
} else {
|
} else {
|
||||||
show_message(res.message, false);
|
show_message(res.message, false);
|
||||||
|
failed();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}"))
|
}"))
|
||||||
|
|
|
@ -20,14 +20,21 @@ pub fn render_markdown(input: &str) -> String {
|
||||||
allowed_attributes.insert("align");
|
allowed_attributes.insert("align");
|
||||||
allowed_attributes.insert("src");
|
allowed_attributes.insert("src");
|
||||||
allowed_attributes.insert("style");
|
allowed_attributes.insert("style");
|
||||||
|
allowed_attributes.insert("controls");
|
||||||
|
allowed_attributes.insert("autoplay");
|
||||||
|
allowed_attributes.insert("loop");
|
||||||
|
|
||||||
tetratto_shared::markdown::clean_html(
|
tetratto_shared::markdown::clean_html(
|
||||||
html.replace("<style>", "<span>:temp_style")
|
html.replace("<style>", "<span>:temp_style")
|
||||||
.replace("</style>", "</span>:temp_style"),
|
.replace("</style>", "</span>:temp_style")
|
||||||
|
.replace("<audio", ":temp_audio<span")
|
||||||
|
.replace("</audio>", "</span>:temp_audio"),
|
||||||
allowed_attributes,
|
allowed_attributes,
|
||||||
)
|
)
|
||||||
.replace("<span>:temp_style", "<style>")
|
.replace("<span>:temp_style", "<style>")
|
||||||
.replace("</span>:temp_style", "</style>")
|
.replace("</span>:temp_style", "</style>")
|
||||||
|
.replace(":temp_audio<span", "<audio")
|
||||||
|
.replace("</span>:temp_audio", "</audio>")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_numeric(value: &str) -> bool {
|
pub(crate) fn is_numeric(value: &str) -> bool {
|
||||||
|
@ -138,6 +145,12 @@ fn parse_highlight_line(output: &mut String, buffer: &mut String, line: &str) {
|
||||||
close_1 = false;
|
close_1 = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if open_1 && char != '=' {
|
||||||
|
buffer.push('=');
|
||||||
|
open_1 = false;
|
||||||
|
is_open = false;
|
||||||
|
}
|
||||||
|
|
||||||
match char {
|
match char {
|
||||||
'=' => {
|
'=' => {
|
||||||
if !is_open {
|
if !is_open {
|
||||||
|
@ -195,7 +208,13 @@ fn parse_underline_line(output: &mut String, buffer: &mut String, line: &str) {
|
||||||
if open_1 && char != '~' {
|
if open_1 && char != '~' {
|
||||||
is_open = false;
|
is_open = false;
|
||||||
open_1 = false;
|
open_1 = false;
|
||||||
buffer.push('!');
|
|
||||||
|
if char == '[' {
|
||||||
|
// image
|
||||||
|
buffer.push('!');
|
||||||
|
} else {
|
||||||
|
buffer.push_str("!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if close_1 && char != '!' {
|
if close_1 && char != '!' {
|
||||||
|
@ -897,6 +916,7 @@ pub fn get_toc_list(input: &str) -> (String, String) {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
let mut toc = String::new();
|
let mut toc = String::new();
|
||||||
let mut in_pre = false;
|
let mut in_pre = false;
|
||||||
|
let mut hc_offset: Option<usize> = None;
|
||||||
|
|
||||||
for line in input.split("\n") {
|
for line in input.split("\n") {
|
||||||
if line.starts_with("```") || line.starts_with("<style>") || line.starts_with("</style>") {
|
if line.starts_with("```") || line.starts_with("<style>") || line.starts_with("</style>") {
|
||||||
|
@ -914,6 +934,7 @@ pub fn get_toc_list(input: &str) -> (String, String) {
|
||||||
if line.starts_with("#") {
|
if line.starts_with("#") {
|
||||||
// get heading count
|
// get heading count
|
||||||
let mut hc = 0;
|
let mut hc = 0;
|
||||||
|
let real_hc;
|
||||||
|
|
||||||
for x in line.chars() {
|
for x in line.chars() {
|
||||||
if x != '#' {
|
if x != '#' {
|
||||||
|
@ -923,8 +944,21 @@ pub fn get_toc_list(input: &str) -> (String, String) {
|
||||||
hc += 1;
|
hc += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
real_hc = hc.clone();
|
||||||
|
if hc_offset.is_none() {
|
||||||
|
if hc > 1 {
|
||||||
|
// offset this count to 1 so the list renders properly
|
||||||
|
hc_offset = Some(hc - 1);
|
||||||
|
hc = 1;
|
||||||
|
} else {
|
||||||
|
hc_offset = Some(0);
|
||||||
|
}
|
||||||
|
} else if let Some(offset) = hc_offset {
|
||||||
|
hc -= offset;
|
||||||
|
}
|
||||||
|
|
||||||
// add heading with id
|
// add heading with id
|
||||||
let x = line.replacen(&"#".repeat(hc), "", 1);
|
let x = line.replacen(&"#".repeat(real_hc), "", 1);
|
||||||
let htext = x.trim();
|
let htext = x.trim();
|
||||||
|
|
||||||
let id = underscore_chars(
|
let id = underscore_chars(
|
||||||
|
@ -933,7 +967,7 @@ pub fn get_toc_list(input: &str) -> (String, String) {
|
||||||
);
|
);
|
||||||
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"<h{hc} id=\"{id}\">{}</h{hc}>\n",
|
"<h{real_hc} id=\"{id}\">{}</h{real_hc}>\n\n",
|
||||||
render_markdown(&htext)
|
render_markdown(&htext)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
42
src/model.rs
42
src/model.rs
|
@ -122,12 +122,24 @@ pub struct EntryMetadata {
|
||||||
#[serde(default, alias = "CONTAINER_PADDING")]
|
#[serde(default, alias = "CONTAINER_PADDING")]
|
||||||
#[validate(max_length = 32)]
|
#[validate(max_length = 32)]
|
||||||
pub container_padding: String,
|
pub container_padding: String,
|
||||||
|
/// The padding of the container on mobile devices.
|
||||||
|
///
|
||||||
|
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/padding>
|
||||||
|
#[serde(default, alias = "CONTAINER_MOBILE_PADDING")]
|
||||||
|
#[validate(max_length = 32)]
|
||||||
|
pub container_mobile_padding: String,
|
||||||
/// The maximum width of the container.
|
/// The maximum width of the container.
|
||||||
///
|
///
|
||||||
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/max-width>
|
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/max-width>
|
||||||
#[serde(default, alias = "CONTAINER_MAX_WIDTH")]
|
#[serde(default, alias = "CONTAINER_MAX_WIDTH")]
|
||||||
#[validate(max_length = 16)]
|
#[validate(max_length = 16)]
|
||||||
pub container_max_width: String,
|
pub container_max_width: String,
|
||||||
|
/// The maximum width of the container on mobile devices.
|
||||||
|
///
|
||||||
|
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/max-width>
|
||||||
|
#[serde(default, alias = "CONTAINER_MOBILE_MAX_WIDTH")]
|
||||||
|
#[validate(max_length = 16)]
|
||||||
|
pub container_mobile_max_width: String,
|
||||||
/// The padding of the container.
|
/// The padding of the container.
|
||||||
/// The color of the text in the inner container.
|
/// The color of the text in the inner container.
|
||||||
#[serde(default, alias = "CONTAINER_INNER_FOREGROUND_COLOR")]
|
#[serde(default, alias = "CONTAINER_INNER_FOREGROUND_COLOR")]
|
||||||
|
@ -257,6 +269,12 @@ pub struct EntryMetadata {
|
||||||
#[serde(default, alias = "CONTAINER_BORDER_WIDTH")]
|
#[serde(default, alias = "CONTAINER_BORDER_WIDTH")]
|
||||||
#[validate(max_length = 16)]
|
#[validate(max_length = 16)]
|
||||||
pub container_border_width: String,
|
pub container_border_width: String,
|
||||||
|
/// The border around the container on mobile devices.
|
||||||
|
///
|
||||||
|
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
|
||||||
|
#[serde(default, alias = "CONTAINER_MOBILE_BORDER_WIDTH")]
|
||||||
|
#[validate(max_length = 16)]
|
||||||
|
pub container_mobile_border_width: String,
|
||||||
/// The border around the container.
|
/// The border around the container.
|
||||||
///
|
///
|
||||||
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-style>
|
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-style>
|
||||||
|
@ -269,6 +287,12 @@ pub struct EntryMetadata {
|
||||||
#[serde(default, alias = "CONTAINER_BORDER_RADIUS")]
|
#[serde(default, alias = "CONTAINER_BORDER_RADIUS")]
|
||||||
#[validate(max_length = 16)]
|
#[validate(max_length = 16)]
|
||||||
pub container_border_radius: String,
|
pub container_border_radius: String,
|
||||||
|
/// The border around the container on mobile devices.
|
||||||
|
///
|
||||||
|
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius>
|
||||||
|
#[serde(default, alias = "CONTAINER_MOBILE_BORDER_RADIUS")]
|
||||||
|
#[validate(max_length = 16)]
|
||||||
|
pub container_mobile_border_radius: String,
|
||||||
/// The shadow around the container.
|
/// The shadow around the container.
|
||||||
///
|
///
|
||||||
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow>
|
/// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow>
|
||||||
|
@ -392,6 +416,18 @@ macro_rules! metadata_css {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
($selector:expr, $property:literal, $self:ident.$field:ident->$output:ident, $media_query:literal) => {
|
||||||
|
if !$self.$field.is_empty() {
|
||||||
|
$output.push_str(&format!(
|
||||||
|
"@media screen and ({}) {{ {} {{ {}: {}; }} }}\n",
|
||||||
|
$media_query,
|
||||||
|
$selector,
|
||||||
|
$property,
|
||||||
|
EntryMetadata::css_escape(&$self.$field)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
($selector:expr, $property:literal, $field:ident->$output:ident) => {
|
($selector:expr, $property:literal, $field:ident->$output:ident) => {
|
||||||
if !$field.is_empty() {
|
if !$field.is_empty() {
|
||||||
$output.push_str(&format!(
|
$output.push_str(&format!(
|
||||||
|
@ -526,7 +562,9 @@ impl EntryMetadata {
|
||||||
let mut output = "<style>".to_string();
|
let mut output = "<style>".to_string();
|
||||||
|
|
||||||
metadata_css!(".container", "padding", self.container_padding->output);
|
metadata_css!(".container", "padding", self.container_padding->output);
|
||||||
|
metadata_css!(".container", "padding", self.container_mobile_padding->output, "max-width: 900px");
|
||||||
metadata_css!(".container", "max-width", self.container_max_width->output);
|
metadata_css!(".container", "max-width", self.container_max_width->output);
|
||||||
|
metadata_css!(".container", "max-width", self.container_mobile_max_width->output, "max-width: 900px");
|
||||||
metadata_css!(".container", "color", self.container_inner_foreground_color->output);
|
metadata_css!(".container", "color", self.container_inner_foreground_color->output);
|
||||||
metadata_css!(".container", "background", self.container_inner_background->output);
|
metadata_css!(".container", "background", self.container_inner_background->output);
|
||||||
metadata_css!(".container", "background-color", self.container_inner_background_color->output);
|
metadata_css!(".container", "background-color", self.container_inner_background_color->output);
|
||||||
|
@ -550,7 +588,9 @@ impl EntryMetadata {
|
||||||
metadata_css!(".container", "border-color", self.container_border_color->output);
|
metadata_css!(".container", "border-color", self.container_border_color->output);
|
||||||
metadata_css!(".container", "border-style", self.container_border_style->output);
|
metadata_css!(".container", "border-style", self.container_border_style->output);
|
||||||
metadata_css!(".container", "border-width", self.container_border_width->output);
|
metadata_css!(".container", "border-width", self.container_border_width->output);
|
||||||
|
metadata_css!(".container", "border-width", self.container_mobile_border_width->output, "max-width: 900px");
|
||||||
metadata_css!(".container", "border-radius", self.container_border_radius->output);
|
metadata_css!(".container", "border-radius", self.container_border_radius->output);
|
||||||
|
metadata_css!(".container", "border-radius", self.container_mobile_border_radius->output, "max-width: 900px");
|
||||||
metadata_css!(".container", "box-shadow", self.container_shadow->output);
|
metadata_css!(".container", "box-shadow", self.container_shadow->output);
|
||||||
metadata_css!(".container", "text-shadow", self.content_text_shadow->output);
|
metadata_css!(".container", "text-shadow", self.content_text_shadow->output);
|
||||||
metadata_css!("*, html *", "--color-link" !important, self.content_link_color->output);
|
metadata_css!("*, html *", "--color-link" !important, self.content_link_color->output);
|
||||||
|
@ -641,7 +681,7 @@ impl EntryMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.content_disable_paragraph_margin {
|
if self.content_disable_paragraph_margin {
|
||||||
output.push_str(".container p { margin: 0; }");
|
output.push_str(".container p { margin: 0 !important; }");
|
||||||
}
|
}
|
||||||
|
|
||||||
output + "</style>"
|
output + "</style>"
|
||||||
|
|
|
@ -146,6 +146,10 @@ async fn view_request(
|
||||||
};
|
};
|
||||||
|
|
||||||
slug = slug.to_lowercase();
|
slug = slug.to_lowercase();
|
||||||
|
let viewed_header = (
|
||||||
|
"Set-Cookie".to_string(),
|
||||||
|
format!("Atto-Viewed=true; Path=/{slug}; Max-Age=86400"),
|
||||||
|
);
|
||||||
|
|
||||||
let entry = match data
|
let entry = match data
|
||||||
.query(&SimplifiedQuery {
|
.query(&SimplifiedQuery {
|
||||||
|
@ -165,7 +169,10 @@ async fn view_request(
|
||||||
&Error::GeneralNotFound("entry".to_string()).to_string(),
|
&Error::GeneralNotFound("entry".to_string()).to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
return (
|
||||||
|
[viewed_header],
|
||||||
|
Html(tera.render("error.lisp", &ctx).unwrap()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -179,7 +186,10 @@ async fn view_request(
|
||||||
if let Err(e) = metadata.validate() {
|
if let Err(e) = metadata.validate() {
|
||||||
let mut ctx = default_context(&data, &build_code);
|
let mut ctx = default_context(&data, &build_code);
|
||||||
ctx.insert("error", &e.to_string());
|
ctx.insert("error", &e.to_string());
|
||||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
return (
|
||||||
|
[viewed_header],
|
||||||
|
Html(tera.render("error.lisp", &ctx).unwrap()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
|
@ -188,7 +198,10 @@ async fn view_request(
|
||||||
{
|
{
|
||||||
let mut ctx = default_context(&data, &build_code);
|
let mut ctx = default_context(&data, &build_code);
|
||||||
ctx.insert("entry", &entry);
|
ctx.insert("entry", &entry);
|
||||||
return Html(tera.render("password.lisp", &ctx).unwrap());
|
return (
|
||||||
|
[viewed_header],
|
||||||
|
Html(tera.render("password.lisp", &ctx).unwrap()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// pull views
|
// pull views
|
||||||
|
@ -205,11 +218,18 @@ async fn view_request(
|
||||||
// count view
|
// count view
|
||||||
let views = r.value.parse::<usize>().unwrap();
|
let views = r.value.parse::<usize>().unwrap();
|
||||||
|
|
||||||
if let Err(e) = data.update(r.id, (views + 1).to_string()).await {
|
if jar.get("Atto-Viewed").is_none() {
|
||||||
let mut ctx = default_context(&data, &build_code);
|
// the Atto-Viewed cookie tells us if we've already viewed this
|
||||||
ctx.insert("error", &e.to_string());
|
// entry recently (at all in the past week)
|
||||||
|
if let Err(e) = data.update(r.id, (views + 1).to_string()).await {
|
||||||
|
let mut ctx = default_context(&data, &build_code);
|
||||||
|
ctx.insert("error", &e.to_string());
|
||||||
|
|
||||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
return (
|
||||||
|
[viewed_header],
|
||||||
|
Html(tera.render("error.lisp", &ctx).unwrap()),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
views
|
views
|
||||||
|
@ -220,7 +240,10 @@ async fn view_request(
|
||||||
let mut ctx = default_context(&data, &build_code);
|
let mut ctx = default_context(&data, &build_code);
|
||||||
ctx.insert("error", &e.to_string());
|
ctx.insert("error", &e.to_string());
|
||||||
|
|
||||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
return (
|
||||||
|
[viewed_header],
|
||||||
|
Html(tera.render("error.lisp", &ctx).unwrap()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -240,10 +263,16 @@ async fn view_request(
|
||||||
|
|
||||||
if metadata.safety_content_warning.is_empty() | qflags.contains(&QuickFlag::AcceptWarning) {
|
if metadata.safety_content_warning.is_empty() | qflags.contains(&QuickFlag::AcceptWarning) {
|
||||||
// regular view
|
// regular view
|
||||||
Html(tera.render("view.lisp", &ctx).unwrap())
|
(
|
||||||
|
[viewed_header],
|
||||||
|
Html(tera.render("view.lisp", &ctx).unwrap()),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// warning
|
// warning
|
||||||
Html(tera.render("warning.lisp", &ctx).unwrap())
|
(
|
||||||
|
[viewed_header],
|
||||||
|
Html(tera.render("warning.lisp", &ctx).unwrap()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue