small changes
This commit is contained in:
+102
-8
@@ -13,6 +13,7 @@ import {
|
||||
expandNamespacedMarkdownLinks,
|
||||
expandTodoMarkup,
|
||||
expandWikiMentions,
|
||||
normalizeMarkdownLinkDestinations,
|
||||
protectCamelCaseWikiWords,
|
||||
extractCmapEmbeds,
|
||||
restoreCmapEmbeds
|
||||
@@ -70,6 +71,8 @@ import { ComboBox } from "./widgets/combobox.js";
|
||||
|
||||
let easyMDE = null;
|
||||
let pendingWikiCmapLinkLabel = "";
|
||||
const sidebarPreferenceKey = "racket-wiki-sidebar-collapsed";
|
||||
const editorSideBySidePreferenceKey = "racket-wiki-editor-side-by-side";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// General UI and HTTP support
|
||||
@@ -133,6 +136,50 @@ import { ComboBox } from "./widgets/combobox.js";
|
||||
document.documentElement.lang = state.language || "en";
|
||||
}
|
||||
|
||||
function setSidebarCollapsed(collapsed, remember = true) {
|
||||
document.body.classList.toggle("sidebar-collapsed", collapsed);
|
||||
const toggle = $("sidebar-toggle");
|
||||
if (!toggle) return;
|
||||
const translationKey = collapsed ? "expand-sidebar" : "collapse-sidebar";
|
||||
toggle.dataset.trTitle = translationKey;
|
||||
toggle.dataset.trAriaLabel = translationKey;
|
||||
toggle.title = tr(translationKey, collapsed ? "Expand sidebar" : "Collapse sidebar");
|
||||
toggle.setAttribute("aria-label", toggle.title);
|
||||
toggle.setAttribute("aria-expanded", String(!collapsed));
|
||||
const icon = document.createElement("i");
|
||||
icon.dataset.lucide = collapsed ? "panel-left-open" : "panel-left-close";
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
const oldIcon = toggle.querySelector("[data-lucide], svg");
|
||||
if (oldIcon) {
|
||||
oldIcon.replaceWith(icon);
|
||||
} else {
|
||||
toggle.append(icon);
|
||||
}
|
||||
if (window.lucide && typeof window.lucide.createIcons === "function") {
|
||||
window.lucide.createIcons();
|
||||
}
|
||||
if (remember) {
|
||||
try {
|
||||
window.sessionStorage.setItem(sidebarPreferenceKey, collapsed ? "true" : "false");
|
||||
} catch (_error) {
|
||||
// A blocked session storage should not prevent the toggle from working.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializeSidebarToggle() {
|
||||
const toggle = $("sidebar-toggle");
|
||||
if (!toggle) return;
|
||||
let collapsed = false;
|
||||
try {
|
||||
collapsed = window.sessionStorage.getItem(sidebarPreferenceKey) === "true";
|
||||
} catch (_error) {
|
||||
collapsed = false;
|
||||
}
|
||||
setSidebarCollapsed(collapsed, false);
|
||||
toggle.addEventListener("click", () => setSidebarCollapsed(!document.body.classList.contains("sidebar-collapsed")));
|
||||
}
|
||||
|
||||
function navigateToHash(targetHash) {
|
||||
const navigate = () => {
|
||||
if (location.hash === targetHash) return route();
|
||||
@@ -204,7 +251,8 @@ import { ComboBox } from "./widgets/combobox.js";
|
||||
|
||||
function renderMarkdown(markdown, pageSlug = null) {
|
||||
const extracted = extractCmapEmbeds(markdown || "");
|
||||
const withExplicitWikiLinks = expandNamespacedMarkdownLinks(extracted.markdown);
|
||||
const normalizedLinks = normalizeMarkdownLinkDestinations(extracted.markdown);
|
||||
const withExplicitWikiLinks = expandNamespacedMarkdownLinks(normalizedLinks);
|
||||
const withWikiLinks = expandWikiMentions(
|
||||
withExplicitWikiLinks,
|
||||
state.pages,
|
||||
@@ -436,6 +484,32 @@ import { ComboBox } from "./widgets/combobox.js";
|
||||
easyMDE.codemirror.refresh();
|
||||
}
|
||||
|
||||
function preferredEditorSideBySide() {
|
||||
try {
|
||||
const stored = window.sessionStorage.getItem(editorSideBySidePreferenceKey);
|
||||
return stored === null ? true : stored === "true";
|
||||
} catch (_error) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function rememberEditorSideBySide() {
|
||||
if (!easyMDE) return;
|
||||
try {
|
||||
window.sessionStorage.setItem(
|
||||
editorSideBySidePreferenceKey,
|
||||
String(easyMDE.isSideBySideActive())
|
||||
);
|
||||
} catch (_error) {
|
||||
// A blocked session storage should not prevent the editor from working.
|
||||
}
|
||||
}
|
||||
|
||||
function toggleEditorSideBySide() {
|
||||
easyMDE.toggleSideBySide();
|
||||
rememberEditorSideBySide();
|
||||
}
|
||||
|
||||
function protectCamelCaseInPage() {
|
||||
if (!easyMDE) return;
|
||||
const source = easyMDE.value();
|
||||
@@ -523,9 +597,10 @@ import { ComboBox } from "./widgets/combobox.js";
|
||||
"|",
|
||||
toolbarButton("undo", EasyMDE.undo, "undo-2", tr("undo", "Undo")),
|
||||
toolbarButton("redo", EasyMDE.redo, "redo-2", tr("redo", "Redo")),
|
||||
toolbarButton("save", savePageFromFullscreen, "save", tr("save", "Save"), { noDisable: true }),
|
||||
toolbarButton("raw-markdown", toggleRawMarkdown, "file-text", tr("raw-markdown", "Raw Markdown"), { noDisable: true }),
|
||||
toolbarButton("preview", EasyMDE.togglePreview, "eye", tr("preview", "Preview"), { noDisable: true }),
|
||||
toolbarButton("side-by-side", EasyMDE.toggleSideBySide, "columns-2", tr("side-by-side", "Side by side"), { noDisable: true, noMobile: true }),
|
||||
toolbarButton("side-by-side", toggleEditorSideBySide, "columns-2", tr("side-by-side", "Side by side"), { noDisable: true, noMobile: true }),
|
||||
toolbarButton("fullscreen", EasyMDE.toggleFullScreen, "maximize", tr("fullscreen", "Fullscreen"), { noDisable: true, noMobile: true })
|
||||
]
|
||||
});
|
||||
@@ -575,7 +650,7 @@ import { ComboBox } from "./widgets/combobox.js";
|
||||
requestAnimationFrame(() => {
|
||||
easyMDE.codemirror.refresh();
|
||||
const wideScreen = window.matchMedia("(min-width: 901px)").matches;
|
||||
if (wideScreen && !easyMDE.isSideBySideActive()) {
|
||||
if (wideScreen && easyMDE.isSideBySideActive() !== preferredEditorSideBySide()) {
|
||||
easyMDE.toggleSideBySide();
|
||||
}
|
||||
updateEditorChromeMetrics();
|
||||
@@ -1045,6 +1120,17 @@ import { ComboBox } from "./widgets/combobox.js";
|
||||
* post : A successful save refreshes page metadata and opens the stored page.
|
||||
*/
|
||||
async function savePage() {
|
||||
return savePageWithOptions();
|
||||
}
|
||||
|
||||
function savePageFromFullscreen() {
|
||||
savePageWithOptions({ keepEditing: true }).catch((error) => {
|
||||
$("save-status").textContent = error.message;
|
||||
});
|
||||
}
|
||||
|
||||
/** Save the current Markdown and optionally keep the editor active. */
|
||||
async function savePageWithOptions({ keepEditing = false } = {}) {
|
||||
const title = $("editor-title").value.trim();
|
||||
const namespace = $("editor-namespace").value.trim();
|
||||
const currentSlug = state.editingNew ? state.newPageSlug : state.currentPage?.slug;
|
||||
@@ -1096,8 +1182,14 @@ import { ComboBox } from "./widgets/combobox.js";
|
||||
applyTranslations();
|
||||
$("account-role").textContent = tr(`role-${state.session.user.role}`, state.session.user.role);
|
||||
}
|
||||
location.hash = `#/${encodeURIComponent(page.slug)}`;
|
||||
await openPage(page.slug);
|
||||
if (keepEditing) {
|
||||
$("editor-namespace").disabled = true;
|
||||
updateEditorSlugInfo();
|
||||
$("edit-summary").value = "";
|
||||
} else {
|
||||
location.hash = `#/${encodeURIComponent(page.slug)}`;
|
||||
await openPage(page.slug);
|
||||
}
|
||||
$("save-status").textContent = tr("saved", "Saved");
|
||||
} catch (error) {
|
||||
$("save-status").textContent = error.message;
|
||||
@@ -1189,7 +1281,7 @@ import { ComboBox } from "./widgets/combobox.js";
|
||||
}
|
||||
const result = await uploadOneFile(file);
|
||||
$("save-status").textContent = tr("image-upload-complete", "Image upload complete");
|
||||
return result.url;
|
||||
return encodeURI(result.url);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1206,9 +1298,10 @@ import { ComboBox } from "./widgets/combobox.js";
|
||||
for (const file of fileList) {
|
||||
const result = await uploadOneFile(file);
|
||||
const escapedName = file.name.replace(/[\[\]]/g, "\\$&");
|
||||
const url = encodeURI(result.url);
|
||||
const markdown = isInlineImage(file)
|
||||
? ``
|
||||
: `[${escapedName}](${result.url})`;
|
||||
? ``
|
||||
: `[${escapedName}](${url})`;
|
||||
insertTextAtCursor(`\n${markdown}\n`);
|
||||
}
|
||||
$("save-status").textContent = tr("upload-complete", "Upload complete");
|
||||
@@ -2023,6 +2116,7 @@ import { ComboBox } from "./widgets/combobox.js";
|
||||
state.translationTemplate = translationData.template || "";
|
||||
state.translations = translationData.translations || {};
|
||||
applyTranslations();
|
||||
initializeSidebarToggle();
|
||||
await cmapWorkspace.initialize();
|
||||
initializeEditor();
|
||||
$("account-name").textContent = state.session.user.displayName;
|
||||
|
||||
@@ -182,6 +182,44 @@ function positionIsProtected(start, end, ranges) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Normalize link destinations that contain whitespace before Marked parses them. */
|
||||
function normalizeMarkdownLinkDestinations(markdown) {
|
||||
const lines = String(markdown || "").split("\n");
|
||||
const result = [];
|
||||
let fence = null;
|
||||
const linkPattern = /(!?\[[^\]\n]*\]\()([^\)\n]*)(\))/g;
|
||||
|
||||
for (const line of lines) {
|
||||
const fenceMatch = line.match(/^\s*(```+|~~~+)/);
|
||||
if (fenceMatch) {
|
||||
const marker = fenceMatch[1].charAt(0);
|
||||
fence = fence === null ? marker : (fence === marker ? null : fence);
|
||||
result.push(line);
|
||||
continue;
|
||||
}
|
||||
if (fence !== null || /^\s{4}/.test(line)) {
|
||||
result.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const codeRanges = [];
|
||||
for (const match of line.matchAll(/`+[^`\n]*`+/g)) {
|
||||
codeRanges.push({ start: match.index, end: match.index + match[0].length });
|
||||
}
|
||||
result.push(line.replace(linkPattern, (match, before, body, after, offset) => {
|
||||
if (positionIsProtected(offset, offset + match.length, codeRanges)) return match;
|
||||
const trimmed = body.trim();
|
||||
if (!trimmed || trimmed.startsWith("<") || !/\s/.test(trimmed)) return match;
|
||||
|
||||
const titleMatch = trimmed.match(/^(.+?)(\s+(?:"[^"\n]*"|'[^'\n]*'|\([^\)\n]*\)))$/);
|
||||
const destination = titleMatch ? titleMatch[1] : trimmed;
|
||||
const title = titleMatch ? titleMatch[2] : "";
|
||||
return `${before}${encodeURI(destination)}${title}${after}`;
|
||||
}));
|
||||
}
|
||||
return result.join("\n");
|
||||
}
|
||||
|
||||
/** Prefix every unprotected CamelCase WikiWord with the literal escape marker. */
|
||||
function protectCamelCaseWikiWords(markdown) {
|
||||
const wikiWordPattern = /(?<![!\p{L}\p{N}._-])((?:\p{Lu}\p{Ll}+){2,})(?![\p{L}\p{N}_-])/gu;
|
||||
@@ -363,6 +401,7 @@ export {
|
||||
expandNamespacedMarkdownLinks,
|
||||
expandTodoMarkup,
|
||||
expandWikiMentions,
|
||||
normalizeMarkdownLinkDestinations,
|
||||
protectCamelCaseWikiWords,
|
||||
extractCmapEmbeds,
|
||||
restoreCmapEmbeds
|
||||
|
||||
Reference in New Issue
Block a user