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': '',
// -> 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() {