Browse Source

make Asciidoc more advanced

imwald
Silberengel 2 weeks ago
parent
commit
377150ebec
  1. 284
      src/components/AdvancedEventLab/AdvancedEventLabMarkupToolbar.tsx
  2. 17
      src/components/AdvancedEventLab/markup-insert.ts
  3. 10
      src/components/Note/AsciidocArticle/AsciidocArticle.tsx
  4. 12
      src/components/Note/MarkdownArticle/preprocessMarkup.ts
  5. 37
      src/i18n/locales/de.ts
  6. 37
      src/i18n/locales/en.ts
  7. 35
      src/lib/asciidoc-double-bracket-guard.ts

284
src/components/AdvancedEventLab/AdvancedEventLabMarkupToolbar.tsx

@ -13,10 +13,13 @@ import { Input } from '@/components/ui/input' @@ -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 { @@ -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({ @@ -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({ @@ -584,11 +604,21 @@ export function AdvancedEventLabMarkupToolbar({
<ChevronDown className="h-3 w-3 opacity-60" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-60">
<DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] max-h-[min(80vh,32rem)] overflow-y-auto">
<DropdownMenuLabel>{t('Advanced lab tb adocTitlesHint')}</DropdownMenuLabel>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, `\n= ${t('Advanced lab tb documentTitle')}\n`))}>
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertRawWithOptionalBlockLeadNl(v, sliceRef, `= ${t('Advanced lab tb documentTitle')}\n`)
)
}
>
{t('Advanced lab tb adocLevel0')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => adocInsertFullHeader(`= ${t('Advanced lab tb documentTitle')}`)}>
{t('Advanced lab tb adocLevel0WithHeader')}
</DropdownMenuItem>
<DropdownMenuSeparator />
{(
[
'Advanced lab tb adocSection1',
@ -662,6 +692,24 @@ export function AdvancedEventLabMarkupToolbar({ @@ -662,6 +692,24 @@ export function AdvancedEventLabMarkupToolbar({
<ImageIcon className="h-3.5 w-3.5 mr-2 inline" />
{t('Advanced lab tb adocImage')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, ' +\n'))}>
{t('Advanced lab tb adocLineBreak')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '^', 'sup'))}>
{t('Advanced lab tb adocSuperscript')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '~', 'sub'))}>
{t('Advanced lab tb adocSubscript')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => run((v) => labInsertSnippet(v, sliceRef, '+++', 'raw or HTML', '+++'))}
>
{t('Advanced lab tb adocPassthrough')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'footnote:[Footnote text]'))}>
{t('Advanced lab tb adocFootnote')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -683,6 +731,20 @@ export function AdvancedEventLabMarkupToolbar({ @@ -683,6 +731,20 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\nterm:: definition line\n'))}>
{t('Advanced lab tb adocLabeled')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertRaw(
v,
sliceRef,
'\n[start=4]\n. fourth item\n. fifth item\n'
)
)
}
>
{t('Advanced lab tb adocOrderedStart')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -759,6 +821,166 @@ export function AdvancedEventLabMarkupToolbar({ @@ -759,6 +821,166 @@ export function AdvancedEventLabMarkupToolbar({
>
{t('Advanced lab tb adocWarning')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertSnippet(
v,
sliceRef,
'\n[IMPORTANT]\n====\n',
'Important body',
'\n====\n'
)
)
}
>
{t('Advanced lab tb adocImportant')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertSnippet(
v,
sliceRef,
'\n[CAUTION]\n====\n',
'Caution body',
'\n====\n'
)
)
}
>
{t('Advanced lab tb adocCaution')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertSnippet(
v,
sliceRef,
'\n[example]\n====\n',
'Example body',
'\n====\n'
)
)
}
>
{t('Advanced lab tb adocExampleBlock')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertSnippet(v, sliceRef, '\n****\n', 'Sidebar body', '\n****\n')
)
}
>
{t('Advanced lab tb adocSidebar')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertSnippet(
v,
sliceRef,
'\n[listing]\n----\n',
'Listing body (often line-oriented text)',
'\n----\n'
)
)
}
>
{t('Advanced lab tb adocListing')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertSnippet(v, sliceRef, '\n--\n', 'Open block body', '\n--\n')
)
}
>
{t('Advanced lab tb adocOpenBlock')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0">
<Anchor className="h-3.5 w-3.5" />
{t('Advanced lab tb adocStructure')}
<ChevronDown className="h-3 w-3 opacity-60" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] max-h-[min(70vh,28rem)] overflow-y-auto">
<DropdownMenuLabel>{t('Advanced lab tb adocStructureHint')}</DropdownMenuLabel>
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertRaw(
v,
sliceRef,
'\n|===\n|Column 1 |Column 2\n\n|Cell A |Cell B\n|===\n'
)
)
}
>
<Table2 className="h-3.5 w-3.5 mr-2 inline" />
{t('Advanced lab tb adocTable')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
run((v) => labInsertRawWithOptionalBlockLeadNl(v, sliceRef, '[#section-anchor]\n'))
}
>
<Hash className="h-3.5 w-3.5 mr-2 inline" />
{t('Advanced lab tb adocAnchor')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertSnippet(v, sliceRef, '<<section-anchor,', 'link label', '>>')
)
}
>
<Link2 className="h-3.5 w-3.5 mr-2 inline" />
{t('Advanced lab tb adocXref')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertRaw(v, sliceRef, '\nvideo::https://example.com/video.mp4[width=640]\n')
)
}
>
<Film className="h-3.5 w-3.5 mr-2 inline" />
{t('Advanced lab tb adocVideo')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertRaw(v, sliceRef, '\naudio::https://example.com/audio.mp3[]\n')
)
}
>
<Volume2 className="h-3.5 w-3.5 mr-2 inline" />
{t('Advanced lab tb adocAudio')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n// Comment line\n'))}>
{t('Advanced lab tb adocComment')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'kbd:[Ctrl+T]'))}>
{t('Advanced lab tb adockbd')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => run((v) => labInsertRaw(v, sliceRef, 'menu:View[Zoom > In]'))}
>
{t('Advanced lab tb adocMenu')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'btn:[OK]'))}>
{t('Advanced lab tb adocBtn')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -817,6 +1039,20 @@ export function AdvancedEventLabMarkupToolbar({ @@ -817,6 +1039,20 @@ export function AdvancedEventLabMarkupToolbar({
>
{t('Advanced lab tb adocStemInline')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'latexmath:[', 'x^2 + y^2', ']'))}
>
{t('Advanced lab tb adocLatexmathInline')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertSnippet(v, sliceRef, '\n[stem]\n++++\n', 'E = mc^2', '\n++++\n')
)
}
>
{t('Advanced lab tb adocStemBlock')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\frac{a}{b}', ']'))}
@ -836,6 +1072,46 @@ export function AdvancedEventLabMarkupToolbar({ @@ -836,6 +1072,46 @@ export function AdvancedEventLabMarkupToolbar({
>
{t('Advanced lab tb katexInt')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
run((v) =>
labInsertRaw(
v,
sliceRef,
'\n[stem]\n++++\n\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}\n++++\n'
)
)
}
>
{t('Advanced lab tb katexMatrix')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
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')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>{t('Advanced lab tb mathGreek')}</DropdownMenuLabel>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\alpha]'))}>
<code className="text-xs mr-2">{'\\alpha'}</code> {t('Advanced lab tb greekAlpha')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\beta]'))}>
<code className="text-xs mr-2">{'\\beta'}</code> {t('Advanced lab tb greekBeta')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\pi]'))}>
<code className="text-xs mr-2">{'\\pi'}</code> {t('Advanced lab tb greekPi')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\infty]'))}>
<code className="text-xs mr-2">{'\\infty'}</code> {t('Advanced lab tb greekInfty')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

17
src/components/AdvancedEventLab/markup-insert.ts

@ -43,6 +43,23 @@ export function labInsertRaw( @@ -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,

10
src/components/Note/AsciidocArticle/AsciidocArticle.tsx

@ -36,6 +36,7 @@ import { @@ -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({ @@ -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}+++`

12
src/components/Note/MarkdownArticle/preprocessMarkup.ts

@ -1,3 +1,4 @@ @@ -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 { @@ -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}+++`
})

37
src/i18n/locales/de.ts

@ -1047,10 +1047,16 @@ export default { @@ -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 <author@example.com>',
'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 { @@ -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 (<<id,Text>>)',
'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',

37
src/i18n/locales/en.ts

@ -1047,10 +1047,16 @@ export default { @@ -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 <author@example.com>',
'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 { @@ -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 (<<id,label>>)',
'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',

35
src/lib/asciidoc-double-bracket-guard.ts

@ -0,0 +1,35 @@ @@ -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
}
Loading…
Cancel
Save