small changes
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(require db
|
(require db
|
||||||
|
net/uri-codec
|
||||||
racket/string)
|
racket/string)
|
||||||
|
|
||||||
(provide replace-current-attachment-references!
|
(provide replace-current-attachment-references!
|
||||||
@@ -27,6 +28,12 @@ SQL
|
|||||||
(define (attachment-url reference stored-name)
|
(define (attachment-url reference stored-name)
|
||||||
(format "/uploads/~a/~a" reference stored-name))
|
(format "/uploads/~a/~a" reference stored-name))
|
||||||
|
|
||||||
|
(define (attachment-url-variants reference stored-name)
|
||||||
|
(list (attachment-url reference stored-name)
|
||||||
|
(format "/uploads/~a/~a"
|
||||||
|
reference
|
||||||
|
(uri-path-segment-unreserved-encode stored-name))))
|
||||||
|
|
||||||
(define (page-references db page-id)
|
(define (page-references db page-id)
|
||||||
(let* ((current
|
(let* ((current
|
||||||
(query-row db
|
(query-row db
|
||||||
@@ -57,7 +64,8 @@ SQL
|
|||||||
(stored-name (vector-ref row 2))
|
(stored-name (vector-ref row 2))
|
||||||
(found? #f))
|
(found? #f))
|
||||||
(for ((reference (in-list (page-references db owner-page-id))))
|
(for ((reference (in-list (page-references db owner-page-id))))
|
||||||
(when (string-contains? markdown (attachment-url reference stored-name))
|
(when (for/or ((url (in-list (attachment-url-variants reference stored-name))))
|
||||||
|
(string-contains? markdown url))
|
||||||
(set! found? #t)))
|
(set! found? #t)))
|
||||||
(when found?
|
(when found?
|
||||||
(query-exec db
|
(query-exec db
|
||||||
@@ -89,6 +97,21 @@ SQL
|
|||||||
(record-references! db page-id sql-null markdown #t referenced-at)
|
(record-references! db page-id sql-null markdown #t referenced-at)
|
||||||
(void))
|
(void))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; Tests for module attachment-references.rkt.
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
(module+ test
|
||||||
|
(require rackunit)
|
||||||
|
|
||||||
|
(define urls
|
||||||
|
(attachment-url-variants
|
||||||
|
"review-zuidas-2026:appreciatie"
|
||||||
|
"1788796826-35348-Review ZuidasDok.docx"))
|
||||||
|
(check-equal?
|
||||||
|
urls
|
||||||
|
(list "/uploads/review-zuidas-2026:appreciatie/1788796826-35348-Review ZuidasDok.docx"
|
||||||
|
"/uploads/review-zuidas-2026:appreciatie/1788796826-35348-Review%20ZuidasDok.docx")))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
; goal : Record attachment references present in an immutable page version.
|
; goal : Record attachment references present in an immutable page version.
|
||||||
; pre : page-version-id identifies the stored version represented by markdown.
|
; pre : page-version-id identifies the stored version represented by markdown.
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ button { cursor: pointer; }
|
|||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .editor-toolbar button.rw-mde-save {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.EasyMDEContainer .CodeMirror,
|
.EasyMDEContainer .CodeMirror,
|
||||||
.EasyMDEContainer .CodeMirror-scroll,
|
.EasyMDEContainer .CodeMirror-scroll,
|
||||||
.EasyMDEContainer .editor-preview-side {
|
.EasyMDEContainer .editor-preview-side {
|
||||||
@@ -222,6 +226,62 @@ body {
|
|||||||
padding: 18px 16px 28px;
|
padding: 18px 16px 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle {
|
||||||
|
display: grid;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
margin: -8px -8px 8px auto;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--wiki-link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle:hover,
|
||||||
|
.sidebar-toggle:focus-visible {
|
||||||
|
background: #fff;
|
||||||
|
border-color: var(--wiki-border);
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
stroke-width: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.sidebar-collapsed #app {
|
||||||
|
grid-template-columns: 52px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.sidebar-collapsed #sidebar {
|
||||||
|
padding: 12px 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.sidebar-collapsed #sidebar > :not(#sidebar-toggle):not(.sidebar-icon-nav) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.sidebar-collapsed #sidebar .sidebar-toggle {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.sidebar-collapsed #sidebar .sidebar-icon-nav {
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 14px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.sidebar-collapsed #sidebar .sidebar-icon-link {
|
||||||
|
width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.sidebar-collapsed #sidebar .sidebar-icon-link + .sidebar-icon-link {
|
||||||
|
border-top: 1px solid #c8cbd0;
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
display: block;
|
display: block;
|
||||||
color: #111;
|
color: #111;
|
||||||
@@ -909,6 +969,10 @@ body.editor-mode .EasyMDEContainer .editor-toolbar.fullscreen {
|
|||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.editor-mode .EasyMDEContainer .editor-toolbar.fullscreen button.rw-mde-save {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
body.editor-mode .EasyMDEContainer .editor-statusbar.fullscreen {
|
body.editor-mode .EasyMDEContainer .editor-statusbar.fullscreen {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
@@ -972,6 +1036,14 @@ body.editor-mode .EasyMDEContainer .editor-preview-side {
|
|||||||
border-top: 0;
|
border-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.editor-mode .editor-shell .EasyMDEContainer:not(:has(.editor-preview-active-side)) {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.editor-mode .editor-shell .EasyMDEContainer:not(:has(.editor-preview-active-side)) .CodeMirror {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
body.editor-mode .EasyMDEContainer .CodeMirror-sided {
|
body.editor-mode .EasyMDEContainer .CodeMirror-sided {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
<div id="connection-status" class="connection-status hidden" role="status"></div>
|
<div id="connection-status" class="connection-status hidden" role="status"></div>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<aside id="sidebar">
|
<aside id="sidebar">
|
||||||
|
<button id="sidebar-toggle" class="sidebar-toggle" type="button" title="Collapse sidebar" data-tr-title="collapse-sidebar" aria-label="Collapse sidebar" data-tr-aria-label="collapse-sidebar" aria-controls="sidebar" aria-expanded="true">
|
||||||
|
<i data-lucide="panel-left-close" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
<a id="wiki-brand" class="brand" href="#">Racket Wiki</a>
|
<a id="wiki-brand" class="brand" href="#">Racket Wiki</a>
|
||||||
<nav class="sidebar-icon-nav" aria-label="Wiki navigation">
|
<nav class="sidebar-icon-nav" aria-label="Wiki navigation">
|
||||||
<a id="bookmarks-link" class="sidebar-icon-link" href="#bookmarks" title="Bookmarks" data-tr-title="bookmarks" aria-label="Bookmarks" data-tr-aria-label="bookmarks"><i data-lucide="star"></i><span class="visually-hidden" data-tr="bookmarks">Bookmarks</span></a>
|
<a id="bookmarks-link" class="sidebar-icon-link" href="#bookmarks" title="Bookmarks" data-tr-title="bookmarks" aria-label="Bookmarks" data-tr-aria-label="bookmarks"><i data-lucide="star"></i><span class="visually-hidden" data-tr="bookmarks">Bookmarks</span></a>
|
||||||
|
|||||||
+102
-8
@@ -13,6 +13,7 @@ import {
|
|||||||
expandNamespacedMarkdownLinks,
|
expandNamespacedMarkdownLinks,
|
||||||
expandTodoMarkup,
|
expandTodoMarkup,
|
||||||
expandWikiMentions,
|
expandWikiMentions,
|
||||||
|
normalizeMarkdownLinkDestinations,
|
||||||
protectCamelCaseWikiWords,
|
protectCamelCaseWikiWords,
|
||||||
extractCmapEmbeds,
|
extractCmapEmbeds,
|
||||||
restoreCmapEmbeds
|
restoreCmapEmbeds
|
||||||
@@ -70,6 +71,8 @@ import { ComboBox } from "./widgets/combobox.js";
|
|||||||
|
|
||||||
let easyMDE = null;
|
let easyMDE = null;
|
||||||
let pendingWikiCmapLinkLabel = "";
|
let pendingWikiCmapLinkLabel = "";
|
||||||
|
const sidebarPreferenceKey = "racket-wiki-sidebar-collapsed";
|
||||||
|
const editorSideBySidePreferenceKey = "racket-wiki-editor-side-by-side";
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// General UI and HTTP support
|
// General UI and HTTP support
|
||||||
@@ -133,6 +136,50 @@ import { ComboBox } from "./widgets/combobox.js";
|
|||||||
document.documentElement.lang = state.language || "en";
|
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) {
|
function navigateToHash(targetHash) {
|
||||||
const navigate = () => {
|
const navigate = () => {
|
||||||
if (location.hash === targetHash) return route();
|
if (location.hash === targetHash) return route();
|
||||||
@@ -204,7 +251,8 @@ import { ComboBox } from "./widgets/combobox.js";
|
|||||||
|
|
||||||
function renderMarkdown(markdown, pageSlug = null) {
|
function renderMarkdown(markdown, pageSlug = null) {
|
||||||
const extracted = extractCmapEmbeds(markdown || "");
|
const extracted = extractCmapEmbeds(markdown || "");
|
||||||
const withExplicitWikiLinks = expandNamespacedMarkdownLinks(extracted.markdown);
|
const normalizedLinks = normalizeMarkdownLinkDestinations(extracted.markdown);
|
||||||
|
const withExplicitWikiLinks = expandNamespacedMarkdownLinks(normalizedLinks);
|
||||||
const withWikiLinks = expandWikiMentions(
|
const withWikiLinks = expandWikiMentions(
|
||||||
withExplicitWikiLinks,
|
withExplicitWikiLinks,
|
||||||
state.pages,
|
state.pages,
|
||||||
@@ -436,6 +484,32 @@ import { ComboBox } from "./widgets/combobox.js";
|
|||||||
easyMDE.codemirror.refresh();
|
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() {
|
function protectCamelCaseInPage() {
|
||||||
if (!easyMDE) return;
|
if (!easyMDE) return;
|
||||||
const source = easyMDE.value();
|
const source = easyMDE.value();
|
||||||
@@ -523,9 +597,10 @@ import { ComboBox } from "./widgets/combobox.js";
|
|||||||
"|",
|
"|",
|
||||||
toolbarButton("undo", EasyMDE.undo, "undo-2", tr("undo", "Undo")),
|
toolbarButton("undo", EasyMDE.undo, "undo-2", tr("undo", "Undo")),
|
||||||
toolbarButton("redo", EasyMDE.redo, "redo-2", tr("redo", "Redo")),
|
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("raw-markdown", toggleRawMarkdown, "file-text", tr("raw-markdown", "Raw Markdown"), { noDisable: true }),
|
||||||
toolbarButton("preview", EasyMDE.togglePreview, "eye", tr("preview", "Preview"), { 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 })
|
toolbarButton("fullscreen", EasyMDE.toggleFullScreen, "maximize", tr("fullscreen", "Fullscreen"), { noDisable: true, noMobile: true })
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -575,7 +650,7 @@ import { ComboBox } from "./widgets/combobox.js";
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
easyMDE.codemirror.refresh();
|
easyMDE.codemirror.refresh();
|
||||||
const wideScreen = window.matchMedia("(min-width: 901px)").matches;
|
const wideScreen = window.matchMedia("(min-width: 901px)").matches;
|
||||||
if (wideScreen && !easyMDE.isSideBySideActive()) {
|
if (wideScreen && easyMDE.isSideBySideActive() !== preferredEditorSideBySide()) {
|
||||||
easyMDE.toggleSideBySide();
|
easyMDE.toggleSideBySide();
|
||||||
}
|
}
|
||||||
updateEditorChromeMetrics();
|
updateEditorChromeMetrics();
|
||||||
@@ -1045,6 +1120,17 @@ import { ComboBox } from "./widgets/combobox.js";
|
|||||||
* post : A successful save refreshes page metadata and opens the stored page.
|
* post : A successful save refreshes page metadata and opens the stored page.
|
||||||
*/
|
*/
|
||||||
async function savePage() {
|
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 title = $("editor-title").value.trim();
|
||||||
const namespace = $("editor-namespace").value.trim();
|
const namespace = $("editor-namespace").value.trim();
|
||||||
const currentSlug = state.editingNew ? state.newPageSlug : state.currentPage?.slug;
|
const currentSlug = state.editingNew ? state.newPageSlug : state.currentPage?.slug;
|
||||||
@@ -1096,8 +1182,14 @@ import { ComboBox } from "./widgets/combobox.js";
|
|||||||
applyTranslations();
|
applyTranslations();
|
||||||
$("account-role").textContent = tr(`role-${state.session.user.role}`, state.session.user.role);
|
$("account-role").textContent = tr(`role-${state.session.user.role}`, state.session.user.role);
|
||||||
}
|
}
|
||||||
location.hash = `#/${encodeURIComponent(page.slug)}`;
|
if (keepEditing) {
|
||||||
await openPage(page.slug);
|
$("editor-namespace").disabled = true;
|
||||||
|
updateEditorSlugInfo();
|
||||||
|
$("edit-summary").value = "";
|
||||||
|
} else {
|
||||||
|
location.hash = `#/${encodeURIComponent(page.slug)}`;
|
||||||
|
await openPage(page.slug);
|
||||||
|
}
|
||||||
$("save-status").textContent = tr("saved", "Saved");
|
$("save-status").textContent = tr("saved", "Saved");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$("save-status").textContent = error.message;
|
$("save-status").textContent = error.message;
|
||||||
@@ -1189,7 +1281,7 @@ import { ComboBox } from "./widgets/combobox.js";
|
|||||||
}
|
}
|
||||||
const result = await uploadOneFile(file);
|
const result = await uploadOneFile(file);
|
||||||
$("save-status").textContent = tr("image-upload-complete", "Image upload complete");
|
$("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) {
|
for (const file of fileList) {
|
||||||
const result = await uploadOneFile(file);
|
const result = await uploadOneFile(file);
|
||||||
const escapedName = file.name.replace(/[\[\]]/g, "\\$&");
|
const escapedName = file.name.replace(/[\[\]]/g, "\\$&");
|
||||||
|
const url = encodeURI(result.url);
|
||||||
const markdown = isInlineImage(file)
|
const markdown = isInlineImage(file)
|
||||||
? ``
|
? ``
|
||||||
: `[${escapedName}](${result.url})`;
|
: `[${escapedName}](${url})`;
|
||||||
insertTextAtCursor(`\n${markdown}\n`);
|
insertTextAtCursor(`\n${markdown}\n`);
|
||||||
}
|
}
|
||||||
$("save-status").textContent = tr("upload-complete", "Upload complete");
|
$("save-status").textContent = tr("upload-complete", "Upload complete");
|
||||||
@@ -2023,6 +2116,7 @@ import { ComboBox } from "./widgets/combobox.js";
|
|||||||
state.translationTemplate = translationData.template || "";
|
state.translationTemplate = translationData.template || "";
|
||||||
state.translations = translationData.translations || {};
|
state.translations = translationData.translations || {};
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
|
initializeSidebarToggle();
|
||||||
await cmapWorkspace.initialize();
|
await cmapWorkspace.initialize();
|
||||||
initializeEditor();
|
initializeEditor();
|
||||||
$("account-name").textContent = state.session.user.displayName;
|
$("account-name").textContent = state.session.user.displayName;
|
||||||
|
|||||||
@@ -182,6 +182,44 @@ function positionIsProtected(start, end, ranges) {
|
|||||||
return false;
|
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. */
|
/** Prefix every unprotected CamelCase WikiWord with the literal escape marker. */
|
||||||
function protectCamelCaseWikiWords(markdown) {
|
function protectCamelCaseWikiWords(markdown) {
|
||||||
const wikiWordPattern = /(?<![!\p{L}\p{N}._-])((?:\p{Lu}\p{Ll}+){2,})(?![\p{L}\p{N}_-])/gu;
|
const wikiWordPattern = /(?<![!\p{L}\p{N}._-])((?:\p{Lu}\p{Ll}+){2,})(?![\p{L}\p{N}_-])/gu;
|
||||||
@@ -363,6 +401,7 @@ export {
|
|||||||
expandNamespacedMarkdownLinks,
|
expandNamespacedMarkdownLinks,
|
||||||
expandTodoMarkup,
|
expandTodoMarkup,
|
||||||
expandWikiMentions,
|
expandWikiMentions,
|
||||||
|
normalizeMarkdownLinkDestinations,
|
||||||
protectCamelCaseWikiWords,
|
protectCamelCaseWikiWords,
|
||||||
extractCmapEmbeds,
|
extractCmapEmbeds,
|
||||||
restoreCmapEmbeds
|
restoreCmapEmbeds
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
expandTodoMarkup,
|
expandTodoMarkup,
|
||||||
expandWikiMentions,
|
expandWikiMentions,
|
||||||
extractCmapEmbeds,
|
extractCmapEmbeds,
|
||||||
|
normalizeMarkdownLinkDestinations,
|
||||||
protectCamelCaseWikiWords,
|
protectCamelCaseWikiWords,
|
||||||
restoreCmapEmbeds
|
restoreCmapEmbeds
|
||||||
} from "../static/js/wiki/markdown.js";
|
} from "../static/js/wiki/markdown.js";
|
||||||
@@ -39,6 +40,13 @@ test("explicit namespaced links become internal routes outside code", () => {
|
|||||||
"[Page](#/notes%3Aroadmap)\n[CMap](#cmap/architecture)\n```\n[Code](notes:keep)\n```");
|
"[Page](#/notes%3Aroadmap)\n[CMap](#cmap/architecture)\n```\n[Code](notes:keep)\n```");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("link destinations with spaces are encoded without changing code", () => {
|
||||||
|
const markdown = "* [DOC](/uploads/review-zuidas-2026:appreciatie/1788796826-35348-Review ZuidasDok - Hans Dijkema - Beschouwing v3.docx)\n`[code](/uploads/page/file name.txt)`\n```\n[code](/uploads/page/file name.txt)\n```";
|
||||||
|
assert.equal(
|
||||||
|
normalizeMarkdownLinkDestinations(markdown),
|
||||||
|
"* [DOC](/uploads/review-zuidas-2026:appreciatie/1788796826-35348-Review%20ZuidasDok%20-%20Hans%20Dijkema%20-%20Beschouwing%20v3.docx)\n`[code](/uploads/page/file name.txt)`\n```\n[code](/uploads/page/file name.txt)\n```");
|
||||||
|
});
|
||||||
|
|
||||||
test("WikiWords use page and CMap catalogues without expanding protected text", () => {
|
test("WikiWords use page and CMap catalogues without expanding protected text", () => {
|
||||||
const pages = [
|
const pages = [
|
||||||
{ slug: "homeopathie", namespace: "", pageSlug: "homeopathie", title: "Homeopathie Wiki" }
|
{ slug: "homeopathie", namespace: "", pageSlug: "homeopathie", title: "Homeopathie Wiki" }
|
||||||
|
|||||||
+2
-2
@@ -144,7 +144,7 @@
|
|||||||
'bulleted-list "Bulleted list" 'numbered-list "Numbered list" 'checklist "Checklist" 'code-block "Code block"
|
'bulleted-list "Bulleted list" 'numbered-list "Numbered list" 'checklist "Checklist" 'code-block "Code block"
|
||||||
'table "Table" 'link "Link" 'horizontal-rule "Horizontal rule" 'undo "Undo" 'redo "Redo"
|
'table "Table" 'link "Link" 'horizontal-rule "Horizontal rule" 'undo "Undo" 'redo "Redo"
|
||||||
'preview "Preview" 'side-by-side "Side by side" 'fullscreen "Fullscreen" 'raw-markdown "Raw Markdown" 'automatic-wiki-link "Automatic wiki link"
|
'preview "Preview" 'side-by-side "Side by side" 'fullscreen "Fullscreen" 'raw-markdown "Raw Markdown" 'automatic-wiki-link "Automatic wiki link"
|
||||||
'language "Language" 'language-en "English" 'language-nl "Dutch"
|
'language "Language" 'language-en "English" 'language-nl "Dutch" 'collapse-sidebar "Collapse sidebar" 'expand-sidebar "Expand sidebar"
|
||||||
'no-headings "No headings" 'uploading "Uploading" 'image-upload-complete "Image upload complete" 'upload-complete "Upload complete"
|
'no-headings "No headings" 'uploading "Uploading" 'image-upload-complete "Image upload complete" 'upload-complete "Upload complete"
|
||||||
'by "by" 'result "result" 'results "results" 'for "for" 'page-does-not-exist "The page does not exist."))
|
'by "by" 'result "result" 'results "results" 'for "for" 'page-does-not-exist "The page does not exist."))
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
'bulleted-list "Opsomming" 'numbered-list "Genummerde lijst" 'checklist "Checklist" 'code-block "Codeblok"
|
'bulleted-list "Opsomming" 'numbered-list "Genummerde lijst" 'checklist "Checklist" 'code-block "Codeblok"
|
||||||
'table "Tabel" 'link "Link" 'horizontal-rule "Horizontale lijn" 'undo "Ongedaan maken" 'redo "Opnieuw uitvoeren"
|
'table "Tabel" 'link "Link" 'horizontal-rule "Horizontale lijn" 'undo "Ongedaan maken" 'redo "Opnieuw uitvoeren"
|
||||||
'preview "Voorbeeld" 'side-by-side "Naast elkaar" 'fullscreen "Volledig scherm" 'raw-markdown "Ruwe Markdown" 'automatic-wiki-link "Automatische wikilink"
|
'preview "Voorbeeld" 'side-by-side "Naast elkaar" 'fullscreen "Volledig scherm" 'raw-markdown "Ruwe Markdown" 'automatic-wiki-link "Automatische wikilink"
|
||||||
'language "Taal" 'language-en "Engels" 'language-nl "Nederlands"
|
'language "Taal" 'language-en "Engels" 'language-nl "Nederlands" 'collapse-sidebar "Zijpaneel inklappen" 'expand-sidebar "Zijpaneel uitklappen"
|
||||||
'no-headings "Geen koppen" 'uploading "Uploaden" 'image-upload-complete "Afbeelding geüpload" 'upload-complete "Upload voltooid"
|
'no-headings "Geen koppen" 'uploading "Uploaden" 'image-upload-complete "Afbeelding geüpload" 'upload-complete "Upload voltooid"
|
||||||
'by "door" 'result "resultaat" 'results "resultaten" 'for "voor" 'page-does-not-exist "De pagina bestaat niet."))
|
'by "door" 'result "resultaat" 'results "resultaten" 'for "voor" 'page-does-not-exist "De pagina bestaat niet."))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user