diff --git a/src/components/AdvancedEventLab/AdvancedEventLabMarkupToolbar.tsx b/src/components/AdvancedEventLab/AdvancedEventLabMarkupToolbar.tsx
index a74b40d1..c5526fef 100644
--- a/src/components/AdvancedEventLab/AdvancedEventLabMarkupToolbar.tsx
+++ b/src/components/AdvancedEventLab/AdvancedEventLabMarkupToolbar.tsx
@@ -13,10 +13,13 @@ import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import type { EditorView } from '@codemirror/view'
import {
+ Anchor,
Braces,
ChevronDown,
Code2,
+ Film,
Heading,
+ Hash,
Image as ImageIcon,
Link2,
List,
@@ -27,12 +30,18 @@ import {
Sigma,
Table2,
Type,
- ListTodo
+ ListTodo,
+ Volume2
} from 'lucide-react'
import type { MutableRefObject } from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { labInsertRaw, labInsertSnippet, labWrapOrSnippet } from './markup-insert'
+import {
+ labInsertRaw,
+ labInsertRawWithOptionalBlockLeadNl,
+ labInsertSnippet,
+ labWrapOrSnippet
+} from './markup-insert'
/** Languages for fenced / source blocks (labels are English; widely recognized by highlighters). */
const CODE_LANGUAGES = [
@@ -112,6 +121,17 @@ export function AdvancedEventLabMarkupToolbar({
fn(v)
}
+ /** Contiguous document header per https://docs.asciidoctor.org/asciidoc/latest/document/header/ (no blank lines until after the last header line). */
+ const adocInsertFullHeader = (titleLine: string) => {
+ run((v) =>
+ labInsertRawWithOptionalBlockLeadNl(
+ v,
+ sliceRef,
+ `${titleLine}\n${t('Advanced lab tb adocHeaderAuthorLine')}\n${t('Advanced lab tb adocHeaderRevisionLine')}\n${t('Advanced lab tb adocHeaderAttrsBlock')}\n\n== ${t('Advanced lab tb sectionTitle')}\n`
+ )
+ )
+ }
+
const mdFence = (lang: string) => {
run((v) =>
labInsertSnippet(v, sliceRef, `\`\`\`${lang}\n`, 'your code here', `\n\`\`\`\n`)
@@ -584,11 +604,21 @@ export function AdvancedEventLabMarkupToolbar({
-
+
{t('Advanced lab tb adocTitlesHint')}
- run((v) => labInsertRaw(v, sliceRef, `\n= ${t('Advanced lab tb documentTitle')}\n`))}>
+
+ run((v) =>
+ labInsertRawWithOptionalBlockLeadNl(v, sliceRef, `= ${t('Advanced lab tb documentTitle')}\n`)
+ )
+ }
+ >
{t('Advanced lab tb adocLevel0')}
+ adocInsertFullHeader(`= ${t('Advanced lab tb documentTitle')}`)}>
+ {t('Advanced lab tb adocLevel0WithHeader')}
+
+
{(
[
'Advanced lab tb adocSection1',
@@ -662,6 +692,24 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocImage')}
+
+ run((v) => labInsertRaw(v, sliceRef, ' +\n'))}>
+ {t('Advanced lab tb adocLineBreak')}
+
+ run((v) => labWrapOrSnippet(v, sliceRef, '^', 'sup'))}>
+ {t('Advanced lab tb adocSuperscript')}
+
+ run((v) => labWrapOrSnippet(v, sliceRef, '~', 'sub'))}>
+ {t('Advanced lab tb adocSubscript')}
+
+ run((v) => labInsertSnippet(v, sliceRef, '+++', 'raw or HTML', '+++'))}
+ >
+ {t('Advanced lab tb adocPassthrough')}
+
+ run((v) => labInsertRaw(v, sliceRef, 'footnote:[Footnote text]'))}>
+ {t('Advanced lab tb adocFootnote')}
+
@@ -683,6 +731,20 @@ export function AdvancedEventLabMarkupToolbar({
run((v) => labInsertRaw(v, sliceRef, '\nterm:: definition line\n'))}>
{t('Advanced lab tb adocLabeled')}
+
+
+ run((v) =>
+ labInsertRaw(
+ v,
+ sliceRef,
+ '\n[start=4]\n. fourth item\n. fifth item\n'
+ )
+ )
+ }
+ >
+ {t('Advanced lab tb adocOrderedStart')}
+
@@ -759,6 +821,166 @@ export function AdvancedEventLabMarkupToolbar({
>
{t('Advanced lab tb adocWarning')}
+
+ run((v) =>
+ labInsertSnippet(
+ v,
+ sliceRef,
+ '\n[IMPORTANT]\n====\n',
+ 'Important body',
+ '\n====\n'
+ )
+ )
+ }
+ >
+ {t('Advanced lab tb adocImportant')}
+
+
+ run((v) =>
+ labInsertSnippet(
+ v,
+ sliceRef,
+ '\n[CAUTION]\n====\n',
+ 'Caution body',
+ '\n====\n'
+ )
+ )
+ }
+ >
+ {t('Advanced lab tb adocCaution')}
+
+
+
+ run((v) =>
+ labInsertSnippet(
+ v,
+ sliceRef,
+ '\n[example]\n====\n',
+ 'Example body',
+ '\n====\n'
+ )
+ )
+ }
+ >
+ {t('Advanced lab tb adocExampleBlock')}
+
+
+ run((v) =>
+ labInsertSnippet(v, sliceRef, '\n****\n', 'Sidebar body', '\n****\n')
+ )
+ }
+ >
+ {t('Advanced lab tb adocSidebar')}
+
+
+ run((v) =>
+ labInsertSnippet(
+ v,
+ sliceRef,
+ '\n[listing]\n----\n',
+ 'Listing body (often line-oriented text)',
+ '\n----\n'
+ )
+ )
+ }
+ >
+ {t('Advanced lab tb adocListing')}
+
+
+ run((v) =>
+ labInsertSnippet(v, sliceRef, '\n--\n', 'Open block body', '\n--\n')
+ )
+ }
+ >
+ {t('Advanced lab tb adocOpenBlock')}
+
+
+
+
+
+
+
+
+
+ {t('Advanced lab tb adocStructureHint')}
+
+ run((v) =>
+ labInsertRaw(
+ v,
+ sliceRef,
+ '\n|===\n|Column 1 |Column 2\n\n|Cell A |Cell B\n|===\n'
+ )
+ )
+ }
+ >
+
+ {t('Advanced lab tb adocTable')}
+
+
+ run((v) => labInsertRawWithOptionalBlockLeadNl(v, sliceRef, '[#section-anchor]\n'))
+ }
+ >
+
+ {t('Advanced lab tb adocAnchor')}
+
+
+ run((v) =>
+ labInsertSnippet(v, sliceRef, '<>')
+ )
+ }
+ >
+
+ {t('Advanced lab tb adocXref')}
+
+
+
+ run((v) =>
+ labInsertRaw(v, sliceRef, '\nvideo::https://example.com/video.mp4[width=640]\n')
+ )
+ }
+ >
+
+ {t('Advanced lab tb adocVideo')}
+
+
+ run((v) =>
+ labInsertRaw(v, sliceRef, '\naudio::https://example.com/audio.mp3[]\n')
+ )
+ }
+ >
+
+ {t('Advanced lab tb adocAudio')}
+
+
+ run((v) => labInsertRaw(v, sliceRef, '\n// Comment line\n'))}>
+ {t('Advanced lab tb adocComment')}
+
+ run((v) => labInsertRaw(v, sliceRef, 'kbd:[Ctrl+T]'))}>
+ {t('Advanced lab tb adockbd')}
+
+ run((v) => labInsertRaw(v, sliceRef, 'menu:View[Zoom > In]'))}
+ >
+ {t('Advanced lab tb adocMenu')}
+
+ run((v) => labInsertRaw(v, sliceRef, 'btn:[OK]'))}>
+ {t('Advanced lab tb adocBtn')}
+
@@ -817,6 +1039,20 @@ export function AdvancedEventLabMarkupToolbar({
>
{t('Advanced lab tb adocStemInline')}
+ run((v) => labInsertSnippet(v, sliceRef, 'latexmath:[', 'x^2 + y^2', ']'))}
+ >
+ {t('Advanced lab tb adocLatexmathInline')}
+
+
+ run((v) =>
+ labInsertSnippet(v, sliceRef, '\n[stem]\n++++\n', 'E = mc^2', '\n++++\n')
+ )
+ }
+ >
+ {t('Advanced lab tb adocStemBlock')}
+
run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\frac{a}{b}', ']'))}
@@ -836,6 +1072,46 @@ export function AdvancedEventLabMarkupToolbar({
>
{t('Advanced lab tb katexInt')}
+
+ run((v) =>
+ labInsertRaw(
+ v,
+ sliceRef,
+ '\n[stem]\n++++\n\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}\n++++\n'
+ )
+ )
+ }
+ >
+ {t('Advanced lab tb katexMatrix')}
+
+
+ run((v) =>
+ labInsertRaw(
+ v,
+ sliceRef,
+ '\n[stem]\n++++\n\\begin{cases} x & x > 0 \\\\ -x & x \\le 0 \\end{cases}\n++++\n'
+ )
+ )
+ }
+ >
+ {t('Advanced lab tb katexCases')}
+
+
+ {t('Advanced lab tb mathGreek')}
+ run((v) => labInsertRaw(v, sliceRef, 'stem:[\\alpha]'))}>
+ {'\\alpha'} {t('Advanced lab tb greekAlpha')}
+
+ run((v) => labInsertRaw(v, sliceRef, 'stem:[\\beta]'))}>
+ {'\\beta'} {t('Advanced lab tb greekBeta')}
+
+ run((v) => labInsertRaw(v, sliceRef, 'stem:[\\pi]'))}>
+ {'\\pi'} {t('Advanced lab tb greekPi')}
+
+ run((v) => labInsertRaw(v, sliceRef, 'stem:[\\infty]'))}>
+ {'\\infty'} {t('Advanced lab tb greekInfty')}
+
diff --git a/src/components/AdvancedEventLab/markup-insert.ts b/src/components/AdvancedEventLab/markup-insert.ts
index 1b1f09d3..f6131240 100644
--- a/src/components/AdvancedEventLab/markup-insert.ts
+++ b/src/components/AdvancedEventLab/markup-insert.ts
@@ -43,6 +43,23 @@ export function labInsertRaw(
labSyncSliceFromView(view, sliceRef)
}
+/** Like {@link labInsertRaw}, but skips a leading newline when the selection starts at document position 0 (avoids an empty first line). */
+export function labInsertRawWithOptionalBlockLeadNl(
+ view: EditorView,
+ sliceRef: { current: AdvancedEventLabSlice | null },
+ body: string
+) {
+ const sel = view.state.selection.main
+ const needsLeadNl = sel.from > 0
+ const insert = needsLeadNl ? `\n${body}` : body
+ view.dispatch({
+ changes: { from: sel.from, to: sel.to, insert },
+ selection: EditorSelection.cursor(sel.from + insert.length)
+ })
+ view.focus()
+ labSyncSliceFromView(view, sliceRef)
+}
+
/** If there is a selection, wrap it; otherwise insert snippet with placeholder between delimiters. */
export function labWrapOrSnippet(
view: EditorView,
diff --git a/src/components/Note/AsciidocArticle/AsciidocArticle.tsx b/src/components/Note/AsciidocArticle/AsciidocArticle.tsx
index 8ef3a460..28c50dcb 100644
--- a/src/components/Note/AsciidocArticle/AsciidocArticle.tsx
+++ b/src/components/Note/AsciidocArticle/AsciidocArticle.tsx
@@ -36,6 +36,7 @@ import {
NOSTR_ASCIIDOC_TEXT_NODE_REGEX,
NOSTR_HTML_BECH32_RELAXED
} from '@/lib/content-patterns'
+import { shouldLeaveDoubleBracketForAsciidoctor } from '@/lib/asciidoc-double-bracket-guard'
import logger from '@/lib/logger'
import { extractBookMetadata } from '@/lib/bookstr-parser'
import { ExtendedKind } from '@/constants'
@@ -396,14 +397,17 @@ export default function AsciidocArticle({
// Then protect regular wikilinks by converting them to passthrough format
// This prevents AsciiDoc from processing them and prevents URLs inside from being processed
- content = content.replace(/\[\[([^\]]+)\]\]/g, (_match, linkContent) => {
+ content = content.replace(/\[\[([^\]]+)\]\]/g, (match, linkContent, offset) => {
// Skip if this was already processed as a bookstr wikilink (shouldn't happen, but safety check)
if (linkContent.startsWith('book::')) {
- return _match
+ return match
}
// Skip citations - they're already processed above
if (linkContent.startsWith('citation::')) {
- return _match
+ return match
+ }
+ if (shouldLeaveDoubleBracketForAsciidoctor(content, offset, match.length, linkContent)) {
+ return match
}
// Convert to AsciiDoc passthrough format so it's preserved
return `+++WIKILINK:${linkContent}+++`
diff --git a/src/components/Note/MarkdownArticle/preprocessMarkup.ts b/src/components/Note/MarkdownArticle/preprocessMarkup.ts
index 061e1977..821d0950 100644
--- a/src/components/Note/MarkdownArticle/preprocessMarkup.ts
+++ b/src/components/Note/MarkdownArticle/preprocessMarkup.ts
@@ -1,3 +1,4 @@
+import { shouldLeaveDoubleBracketForAsciidoctor } from '@/lib/asciidoc-double-bracket-guard'
import { isImage, isVideo, isAudio } from '@/lib/url'
import { URL_REGEX, YOUTUBE_URL_REGEX } from '@/constants'
import { isSpotifyOpenUrl } from '@/lib/spotify-url'
@@ -137,10 +138,15 @@ export function preprocessAsciidocMediaLinks(content: string): string {
})
// Fallback: protect regular wikilinks if they weren't processed yet
- processed = processed.replace(/\[\[([^\]]+)\]\]/g, (_match, linkContent) => {
- // Skip if this was already processed as a bookstr wikilink
+ processed = processed.replace(/\[\[([^\]]+)\]\]/g, (match, linkContent, offset) => {
if (linkContent.startsWith('book::')) {
- return _match
+ return match
+ }
+ if (linkContent.startsWith('citation::')) {
+ return match
+ }
+ if (shouldLeaveDoubleBracketForAsciidoctor(processed, offset, match.length, linkContent)) {
+ return match
}
return `+++WIKILINK:${linkContent}+++`
})
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index 4b591e37..b355446a 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -1047,10 +1047,16 @@ export default {
'Advanced lab tb greekInfty': 'Unendlich',
'Advanced lab tb hrTitle': 'Horizontale Linie',
'Advanced lab tb adocTitles': 'Titel',
- 'Advanced lab tb adocTitlesHint': 'AsciiDoc: = Dokumenttitel, == … für Abschnitte.',
+ 'Advanced lab tb adocTitlesHint':
+ 'Dokumentkopf: keine Leerzeilen innerhalb des Kopfes; die erste Leerzeile beendet ihn (https://docs.asciidoctor.org/asciidoc/latest/document/header/).',
'Advanced lab tb documentTitle': 'Dokumenttitel',
'Advanced lab tb sectionTitle': 'Abschnittstitel',
'Advanced lab tb adocLevel0': 'Dokumenttitel (=)',
+ 'Advanced lab tb adocLevel0WithHeader': 'Minimaler Kopf (Titel, Autor, Revision, Beschreibung, Stichwörter, erster Abschnitt)',
+ 'Advanced lab tb adocHeaderAuthorLine': 'Autor Name ',
+ 'Advanced lab tb adocHeaderRevisionLine': '1.0, 2026-04-15: Entwurf',
+ 'Advanced lab tb adocHeaderAttrsBlock':
+ ':description: Kurze Zusammenfassung für Metadaten\n:keywords: stichwort-eins, stichwort-zwei, stichwort-drei',
'Advanced lab tb adocSection1': 'Abschnitt (==)',
'Advanced lab tb adocSection2': 'Unterabschnitt (===)',
'Advanced lab tb adocSection3': 'Ebene 4 (====)',
@@ -1073,8 +1079,35 @@ export default {
'Advanced lab tb adocSource': 'Source / Listing',
'Advanced lab tb adocSourceHint': 'Fügt [source,Sprache] ein; Beispielcode ersetzen.',
'Advanced lab tb adocStem': 'STEM / LaTeX',
- 'Advanced lab tb adocStemHint': 'stem:[…] (STEM in der Pipeline ggf. aktivieren).',
+ 'Advanced lab tb adocStemHint':
+ 'Imwald nutzt Asciidoctor mit stem: latexmath; Formeln werden zu \\(...\\) / \\[...\\] und mit KaTeX gerendert (nicht MathJax).',
'Advanced lab tb adocStemInline': 'Inline stem:[…]',
+ 'Advanced lab tb adocLatexmathInline': 'Inline latexmath:[…]',
+ 'Advanced lab tb adocStemBlock': 'Display-Formel ([stem] +++ … +++++)',
+ 'Advanced lab tb adocLineBreak': 'Zeilenumbruch (Leerzeichen + am Zeilenende)',
+ 'Advanced lab tb adocSubscript': 'Tiefgestellt (~text~)',
+ 'Advanced lab tb adocSuperscript': 'Hochgestellt (^text^)',
+ 'Advanced lab tb adocPassthrough': 'Inline-Passthrough (+++ … +++)',
+ 'Advanced lab tb adocFootnote': 'Fußnote (footnote:[…])',
+ 'Advanced lab tb adocImportant': 'IMPORTANT-Hinweis',
+ 'Advanced lab tb adocCaution': 'CAUTION-Hinweis',
+ 'Advanced lab tb adocExampleBlock': 'Beispielblock ([example])',
+ 'Advanced lab tb adocSidebar': 'Seitenleiste (**** … ****)',
+ 'Advanced lab tb adocListing': 'Listing-Block ([listing] + ----)',
+ 'Advanced lab tb adocOpenBlock': 'Open-Block (-- … --)',
+ 'Advanced lab tb adocStructure': 'Struktur & Medien',
+ 'Advanced lab tb adocStructureHint':
+ 'Tabellen, IDs/Anker ([#id] vor einem Block—kollidiert nicht mit [[Wikilink]]-Syntax), Querverweise, Medien (Asciidoctor-Blockmakros).',
+ 'Advanced lab tb adocTable': 'Tabelle (|=== …)',
+ 'Advanced lab tb adocAnchor': 'Block- oder Abschnitts-ID ([#id])',
+ 'Advanced lab tb adocXref': 'Querverweis (<>)',
+ 'Advanced lab tb adocVideo': 'Video (video::url[])',
+ 'Advanced lab tb adocAudio': 'Audio (audio::url[])',
+ 'Advanced lab tb adocOrderedStart': 'Nummerierte Liste mit [start=n]',
+ 'Advanced lab tb adocComment': 'Zeilenkommentar (//)',
+ 'Advanced lab tb adockbd': 'Tastatur (kbd:[…])',
+ 'Advanced lab tb adocMenu': 'Menüpfad (menu:…)',
+ 'Advanced lab tb adocBtn': 'Schaltfläche (btn:[…])',
'Advanced lab tb adocHrTitle': 'Thematic break (\'\'\')',
Apply: 'Anwenden',
Reset: 'Zurücksetzen',
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index e4bfd5e9..44f5a026 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -1047,10 +1047,16 @@ export default {
'Advanced lab tb greekInfty': 'infinity',
'Advanced lab tb hrTitle': 'Horizontal rule',
'Advanced lab tb adocTitles': 'Titles',
- 'Advanced lab tb adocTitlesHint': 'AsciiDoc uses = for the document title and == … for sections.',
+ 'Advanced lab tb adocTitlesHint':
+ 'Document header: no empty lines inside the header; the first blank line ends it (https://docs.asciidoctor.org/asciidoc/latest/document/header/).',
'Advanced lab tb documentTitle': 'Document title',
'Advanced lab tb sectionTitle': 'Section title',
'Advanced lab tb adocLevel0': 'Document title (=)',
+ 'Advanced lab tb adocLevel0WithHeader': 'Minimal header (title, author, revision, description, keywords, first section)',
+ 'Advanced lab tb adocHeaderAuthorLine': 'Author Name ',
+ 'Advanced lab tb adocHeaderRevisionLine': '1.0, 2026-04-15: Draft revision',
+ 'Advanced lab tb adocHeaderAttrsBlock':
+ ':description: Short document summary for metadata\n:keywords: keyword-one, keyword-two, keyword-three',
'Advanced lab tb adocSection1': 'Section (==)',
'Advanced lab tb adocSection2': 'Subsection (===)',
'Advanced lab tb adocSection3': 'Subsubsection (====)',
@@ -1073,8 +1079,35 @@ export default {
'Advanced lab tb adocSource': 'Source / listing',
'Advanced lab tb adocSourceHint': 'Inserts a [source,lang] block; replace the sample code.',
'Advanced lab tb adocStem': 'STEM / LaTeX',
- 'Advanced lab tb adocStemHint': 'Asciidoctor stem:[…] (enable stem in your pipeline if needed).',
+ 'Advanced lab tb adocStemHint':
+ 'Imwald runs Asciidoctor with stem: latexmath; math becomes \\(...\\) / \\[...\\] and is drawn with KaTeX (not MathJax).',
'Advanced lab tb adocStemInline': 'Inline stem:[…]',
+ 'Advanced lab tb adocLatexmathInline': 'Inline latexmath:[…]',
+ 'Advanced lab tb adocStemBlock': 'Display math ([stem] +++ … +++++)',
+ 'Advanced lab tb adocLineBreak': 'Hard line break (space + at line end)',
+ 'Advanced lab tb adocSubscript': 'Subscript (~text~)',
+ 'Advanced lab tb adocSuperscript': 'Superscript (^text^)',
+ 'Advanced lab tb adocPassthrough': 'Inline passthrough (+++ … +++)',
+ 'Advanced lab tb adocFootnote': 'Footnote (footnote:[…])',
+ 'Advanced lab tb adocImportant': 'IMPORTANT admonition',
+ 'Advanced lab tb adocCaution': 'CAUTION admonition',
+ 'Advanced lab tb adocExampleBlock': 'Example block ([example])',
+ 'Advanced lab tb adocSidebar': 'Sidebar (**** … ****)',
+ 'Advanced lab tb adocListing': 'Listing block ([listing] + ----)',
+ 'Advanced lab tb adocOpenBlock': 'Open block (-- … --)',
+ 'Advanced lab tb adocStructure': 'Structure & media',
+ 'Advanced lab tb adocStructureHint':
+ 'Tables, IDs/anchors ([#id] before a block—avoids [[wikilink]] syntax), cross-refs, media (Asciidoctor block macros).',
+ 'Advanced lab tb adocTable': 'Table (|=== …)',
+ 'Advanced lab tb adocAnchor': 'Block or section ID ([#id])',
+ 'Advanced lab tb adocXref': 'Cross reference (<>)',
+ 'Advanced lab tb adocVideo': 'Video (video::url[])',
+ 'Advanced lab tb adocAudio': 'Audio (audio::url[])',
+ 'Advanced lab tb adocOrderedStart': 'Ordered list with [start=n]',
+ 'Advanced lab tb adocComment': 'Line comment (//)',
+ 'Advanced lab tb adockbd': 'Keyboard (kbd:[…])',
+ 'Advanced lab tb adocMenu': 'Menu path (menu:…)',
+ 'Advanced lab tb adocBtn': 'Button (btn:[…])',
'Advanced lab tb adocHrTitle': 'Thematic break (\'\'\')',
Apply: 'Apply',
Reset: 'Reset',
diff --git a/src/lib/asciidoc-double-bracket-guard.ts b/src/lib/asciidoc-double-bracket-guard.ts
new file mode 100644
index 00000000..7374f346
--- /dev/null
+++ b/src/lib/asciidoc-double-bracket-guard.ts
@@ -0,0 +1,35 @@
+/**
+ * `[[inner]]` is both a wiki link (Nostr / bookstr) and valid AsciiDoc (block anchor, biblio id, etc.).
+ * When we rewrite every `[[…]]` to a wiki passthrough, AsciiDoc `[[id]]` anchors break; prefer `[#id]` before a block to avoid clashing with `[[wikilink]]`.
+ *
+ * Leave the original brackets for Asciidoctor when this looks like an AsciiDoc anchor, not a wiki slug.
+ * @see https://docs.asciidoctor.org/asciidoc/latest/syntax/ids/
+ */
+export function shouldLeaveDoubleBracketForAsciidoctor(
+ fullSource: string,
+ matchIndex: number,
+ matchLength: number,
+ inner: string
+): boolean {
+ const t = inner.trim()
+ if (!t) return false
+
+ // Bibliographic / paired id (not wiki `target|display`)
+ if (t.includes(',') && !t.includes('|')) return true
+
+ const lineStart = fullSource.lastIndexOf('\n', matchIndex - 1) + 1
+ const lineEndIdx = fullSource.indexOf('\n', matchIndex + matchLength)
+ const lineEnd = lineEndIdx === -1 ? fullSource.length : lineEndIdx
+ const lineTrimmed = fullSource.slice(lineStart, lineEnd).trim()
+ const matchTrimmed = fullSource.slice(matchIndex, matchIndex + matchLength).trim()
+ if (lineTrimmed !== matchTrimmed) return false
+
+ const after = fullSource.slice(matchIndex + matchLength)
+ if (!/^\s*\n(?:\s*\n)*={2,6}\s\S/.test(after)) return false
+
+ // Lowercase slug + section ahead: legacy `[[id]]` block anchor (e.g. before == Intro); new content should use `[#id]`.
+ // Uppercase wiki slugs like [[NIP-54]] still go through wiki passthrough.
+ if (!/^[a-z][a-z0-9._-]*$/.test(t)) return false
+
+ return true
+}