From f64d6431232c887f39b1e099c0c67e20984e2156 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Fri, 25 Sep 2026 14:08:58 -0400 Subject: [PATCH] feat: wikilinks syntax + more compact ToC rail --- backend/base.yml | 1 + backend/locales/en.json | 2 + backend/models/sites.ts | 6 +- frontend/scripts/check-visual-roundtrip.mjs | 52 +++++- .../EditorMarkdownConfigOverlay.vue | 19 +- frontend/src/components/PageToc.vue | 82 ++++++++- frontend/src/css/_page-chrome.scss | 10 ++ frontend/src/editor/visual/links.js | 4 + frontend/src/editor/visual/parse.js | 19 +- frontend/src/editor/visual/schema.js | 5 +- frontend/src/editor/visual/serialize.js | 42 ++++- frontend/src/pages/Index.vue | 28 ++- frontend/src/renderers/markdown.js | 22 ++- .../modules/markdown-it-wikilinks.js | 168 ++++++++++++++++++ 14 files changed, 432 insertions(+), 28 deletions(-) create mode 100644 frontend/src/renderers/modules/markdown-it-wikilinks.js diff --git a/backend/base.yml b/backend/base.yml index a3e991ea1..cd6d8e7fa 100644 --- a/backend/base.yml +++ b/backend/base.yml @@ -224,6 +224,7 @@ editors: lineBreaks: true typographer: false underline: false + wikiLinks: true tabWidth: 2 latexEngine: katex kroki: true diff --git a/backend/locales/en.json b/backend/locales/en.json index e2a5fda46..c27380cc9 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -437,6 +437,8 @@ "admin.editors.markdown.typographerHint": "Enable some language-neutral replacement + quotes beautification.", "admin.editors.markdown.underline": "Underline Emphasis", "admin.editors.markdown.underlineHint": "Enable text underlining by using _underline_ syntax.", + "admin.editors.markdown.wikiLinks": "Wikilinks", + "admin.editors.markdown.wikiLinksHint": "Link to pages by name with the [[Page Name]] and [[Page Name{'|'}text]] syntax. Spaces become dashes and letters are lowercased.", "admin.editors.markdownDescription": "Use the Markdown syntax to write content. Includes real-time preview and code completion features.", "admin.editors.markdownName": "Markdown Editor", "admin.editors.redirectDescription": "Create redirections to other pages / external links.", diff --git a/backend/models/sites.ts b/backend/models/sites.ts index 85f16385a..df951444d 100644 --- a/backend/models/sites.ts +++ b/backend/models/sites.ts @@ -231,7 +231,8 @@ class Sites { quotes: 'english', tabWidth: 2, typographer: false, - underline: true + underline: true, + wikiLinks: true } }, /* @@ -526,7 +527,8 @@ class Sites { quotes: 'english', tabWidth: 2, typographer: false, - underline: true + underline: true, + wikiLinks: true } }, /* diff --git a/frontend/scripts/check-visual-roundtrip.mjs b/frontend/scripts/check-visual-roundtrip.mjs index 1a72f14b5..13d6e92a0 100644 --- a/frontend/scripts/check-visual-roundtrip.mjs +++ b/frontend/scripts/check-visual-roundtrip.mjs @@ -79,13 +79,20 @@ const VERBOSE = process.argv.includes('--verbose') * an editor that normalised it to `*x*` would be changing the page rather than reformatting it. */ const CONFIGS = { - default: { allowHTML: true, linkify: true, lineBreaks: true, multimdTable: true }, + default: { + allowHTML: true, + linkify: true, + lineBreaks: true, + multimdTable: true, + wikiLinks: true + }, underline: { allowHTML: true, linkify: true, lineBreaks: true, multimdTable: true, - underline: true + underline: true, + wikiLinks: true }, plain: { allowHTML: true, @@ -111,6 +118,10 @@ const CONSTRUCTS = { */ 'link opening a new tab': 'A [new tab](https://example.com){target="_blank"} here.', 'link with id and class': 'A [classed](https://example.com){#x .cls} here.', + // -> A citation written as a link, which crashed the renderer before MDC's span stopped claiming + // the brackets inside it + 'link whose text is bracketed': 'See [[1]](https://example.com) here.', + 'link with a span in its text': 'A [text with [a span]{.x} in it](https://example.com) here.', 'link with a title and a target': 'A [both](https://example.com "Tip"){target="_blank"} here.', 'image with size': '![alt](pic.png =100x200)', // -> Both halves of the suffix are optional, and a height on its own went missing on the way back @@ -123,6 +134,7 @@ const CONSTRUCTS = { 'ordered list with parens': '1) one\n2) two', 'loose list': '- one\n\n- two', 'task list': '- [x] done\n- [ ] todo', + 'task starting with a link': '- [ ] [a link](/somewhere) to do', blockquote: '> quoted\n>\n> second', alert: '> [!WARNING] Mind the gap\n> Body of the alert.', 'alert without a title': '> [!NOTE]\n> Body of the note.', @@ -154,6 +166,18 @@ const CONSTRUCTS = { 'code containing backticks': '``a ` b``' } +/** + * Only under a config that turns `wikiLinks` on. With it off, `[[x]]` is two of MDC's inline spans one + * inside the other, which is a different construct and not what these are here to check. + */ +const WIKILINK_CONSTRUCTS = { + wikilink: 'See [[Getting Started]] and [[Guides/Setup Guide|the setup guide]].', + 'wikilink to a section': 'See [[Page Name#Some Heading]] and [[#Local Section]].', + 'wikilink with formatted text': 'See [[Some Page|**bold** and *italic*]] here.', + 'wikilink with escapes': 'See [[Star\\* Page]] here.', + 'wikilink opening a new tab': 'See [[Some Page]]{target="_blank"} here.' +} + /** * A render reduced to what it says. * @@ -182,6 +206,27 @@ function check(name, source, config) { } catch (err) { return { ok: false, why: `threw while round-tripping: ${err.message}` } } + /* + And a second save must change nothing at all. The render comparison above cannot see a source that + only drifts in its whitespace -- it collapses whitespace on purpose -- and that is exactly the + failure that grows: a task item gained one more space after its checkbox on every save. + */ + let again + try { + again = serialize(parser.parse(rewritten, 'demo/page')) + } catch (err) { + return { ok: false, why: `threw on a second round trip: ${err.message}`, rewritten } + } + if (again !== rewritten) { + return { + ok: false, + why: 'changes again on a second round trip', + rewritten, + before: rewritten, + after: again + } + } + const before = meaningOf(render(source)) const after = meaningOf(render(rewritten)) if (before === after) { @@ -206,6 +251,9 @@ let checked = 0 for (const [configName, config] of Object.entries(CONFIGS)) { const cases = Object.entries(CONSTRUCTS) + if (config.wikiLinks) { + cases.push(...Object.entries(WIKILINK_CONSTRUCTS)) + } for (const page of SAMPLE_PAGES) { if (page.content?.trim()) { cases.push([`sample page: ${page.title ?? page.path}`, page.content]) diff --git a/frontend/src/components/EditorMarkdownConfigOverlay.vue b/frontend/src/components/EditorMarkdownConfigOverlay.vue index 325b9559b..92d5f2caf 100644 --- a/frontend/src/components/EditorMarkdownConfigOverlay.vue +++ b/frontend/src/components/EditorMarkdownConfigOverlay.vue @@ -186,6 +186,22 @@ :aria-label="t(`admin.editors.markdown.underline`)" /> + + + + + {{t(`admin.editors.markdown.wikiLinks`)}} + {{t(`admin.editors.markdown.wikiLinksHint`)}} + + + + + @@ -233,7 +249,8 @@ function defaultConfig() { quotes: 'english', underline: true, tabWidth: 2, - multimdTable: true + multimdTable: true, + wikiLinks: true } } diff --git a/frontend/src/components/PageToc.vue b/frontend/src/components/PageToc.vue index 7c900d360..b6e127ac0 100644 --- a/frontend/src/components/PageToc.vue +++ b/frontend/src/components/PageToc.vue @@ -1,5 +1,5 @@ @@ -689,6 +690,15 @@ const tocPanelIsOpen = computed(() => tocIsPanel.value && showSidebar.value && s */ const showTocPanelBtn = computed(() => tocIsPanel.value && showSidebar.value && !state.tocPanelOpen) +/* + Whether the contents rail runs out to meet the article column's right-hand edge (`.page-article-col`), + which is only beside it while the contents are a column to the RIGHT of the article. On the left the + edge is on the article's far side, and the panel has a shadow rather than an edge to meet. +*/ +const tocJoinsArticleEdge = computed( + () => !tocIsPanel.value && siteStore.theme.tocPosition !== 'left' +) + /** * Whether the page on screen is a blog's front page, which is drawn as its posts rather than as an * article -- see `PageBlog.vue`. A blog POST is an ordinary page and is not this. @@ -1440,17 +1450,20 @@ function goBack() {