From 7e828e2b69f9083e7ddcf9372b4ca59c58e059c9 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 1 Sep 2026 13:01:45 +1000 Subject: [PATCH 1/4] fix(markdown): indent nested lists and drop the blank line between items fixes #115 --- src/parser.ts | 62 ++++++++++------ test/fixtures/macros/src/macros.h | 21 ++++++ test/fixtures/macros/xml-out/xml/index.xml | 1 + .../fixtures/macros/xml-out/xml/macros_8h.xml | 71 +++++++++++++++---- .../macros/xml-out/xml/structhandlers.xml | 6 +- test/snapshots/macros/api.md | 36 +++++++++- test/snapshots/macros/handlers.md | 6 +- 7 files changed, 159 insertions(+), 44 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index d2783c86..2f578e43 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -36,6 +36,36 @@ function currentContextName(context: XmlElement[]): string | undefined { return context[context.length - 1]?.['#name']; } +const LIST_ELEMENTS = new Set(['orderedlist', 'itemizedlist', 'variablelist']); + +function listMarker(name: string | undefined): string { + return name === 'orderedlist' ? '1. ' : name === 'variablelist' ? ': ' : '* '; +} + +function enclosingLists(context: XmlElement[]): XmlElement[] { + return context.filter((el) => LIST_ELEMENTS.has(el['#name'] ?? '')); +} + +/** + * Indent for a list item, which Markdown measures against the marker of the + * item it nests under, so an ordered parent indents further than a bullet. + */ +function listIndent(context: XmlElement[]): string { + return enclosingLists(context) + .slice(0, -1) + .map((el) => ' '.repeat(listMarker(el['#name']).length)) + .join(''); +} + +function insideList(context: XmlElement[]): boolean { + return LIST_ELEMENTS.has(currentContextName(context) ?? ''); +} + +/** Markdown needs exactly one newline between list items, never a blank one. */ +function endLine(text: string): string { + return text.endsWith('\n') ? text : `${text}\n`; +} + function headingLevel(context: XmlElement[]): number { const parent = context[context.length - 1]; const match = parent?.['#name']?.match(/^sect([1-6])$/); @@ -112,16 +142,12 @@ function toMarkdown(element: unknown, context: XmlElement[] = [], options: Markd s = '\n```\n'; break; case 'orderedlist': - context.push(el); - s = '\n\n'; - break; case 'itemizedlist': - context.push(el); - s = '\n\n'; - break; case 'variablelist': + // A nested list continues the item above it rather than starting a + // fresh block, so it opens on the next line, not after a blank one. + s = enclosingLists(context).length > 0 ? '\n' : '\n\n'; context.push(el); - s = '\n\n'; break; case 'varlistentry': s = '\n* '; @@ -130,11 +156,7 @@ function toMarkdown(element: unknown, context: XmlElement[] = [], options: Markd s = '**'; break; case 'listitem': - s = currentContextName(context) === 'orderedlist' - ? '1. ' - : currentContextName(context) === 'variablelist' - ? ': ' - : '* '; + s = listIndent(context) + listMarker(currentContextName(context)); break; case 'sp': s = ' '; @@ -260,9 +282,13 @@ function toMarkdown(element: unknown, context: XmlElement[] = [], options: Markd break; } case 'parameterlist': - case 'para': s += '\n\n'; break; + case 'para': + // A paragraph is a block on its own, except inside a list item, where a + // blank line would break the item away from its own list. + s = insideList(context) ? endLine(s) : `${s}\n\n`; + break; case 'emphasis': s += '*'; break; @@ -296,19 +322,13 @@ function toMarkdown(element: unknown, context: XmlElement[] = [], options: Markd s = md.safeLink(s, el.$?.url ?? ''); break; case 'orderedlist': - context.pop(); - s += '\n'; - break; case 'itemizedlist': - context.pop(); - s += '\n'; - break; case 'variablelist': context.pop(); - s += '\n'; + s = endLine(s); break; case 'listitem': - s += '\n'; + s = endLine(s); break; case 'term': s += '**'; diff --git a/test/fixtures/macros/src/macros.h b/test/fixtures/macros/src/macros.h index c4a41d5d..1c53cfaa 100644 --- a/test/fixtures/macros/src/macros.h +++ b/test/fixtures/macros/src/macros.h @@ -27,6 +27,27 @@ */ void reset(int); +/** + Drain the queue. + + Ordering rules: + + - first + - first.a + - first.b + - second + - second.a + - third + + Steps: + + 1. open + 1. acquire + 2. verify + 2. close +*/ +void drain(void); + /** Registry of handler hooks. */ diff --git a/test/fixtures/macros/xml-out/xml/index.xml b/test/fixtures/macros/xml-out/xml/index.xml index 0e7b8009..4fe74883 100644 --- a/test/fixtures/macros/xml-out/xml/index.xml +++ b/test/fixtures/macros/xml-out/xml/index.xml @@ -12,6 +12,7 @@ RETRY_HOOK registry reset + drain configure src diff --git a/test/fixtures/macros/xml-out/xml/macros_8h.xml b/test/fixtures/macros/xml-out/xml/macros_8h.xml index 6d459ee4..20d83b13 100644 --- a/test/fixtures/macros/xml-out/xml/macros_8h.xml +++ b/test/fixtures/macros/xml-out/xml/macros_8h.xml @@ -91,7 +91,7 @@ - + @@ -106,7 +106,7 @@ - + @@ -127,6 +127,47 @@ + + void + void drain + (void) + drain + + void + + + + +Drain the queue. +Ordering rules: + +first +first.a +first.b + + +second +second.a + + +third + + +Steps: + +open +acquire +verify + + +close + + + + + + + struct handlers * struct handlers * configure @@ -167,7 +208,7 @@ - + @@ -188,17 +229,19 @@ voidreset(int); -structhandlers{ -int(*retry)(intattempt,constchar*reason); - -structhandlers*fallback; -}*registry; - -#defineRETRY_HOOKregistry->retry - -structhandlers*configure(structhandlers*target,intcount); - -#endif/*__MACROS_H__*/ +voiddrain(void); + +structhandlers{ +int(*retry)(intattempt,constchar*reason); + +structhandlers*fallback; +}*registry; + +#defineRETRY_HOOKregistry->retry + +structhandlers*configure(structhandlers*target,intcount); + +#endif/*__MACROS_H__*/ diff --git a/test/fixtures/macros/xml-out/xml/structhandlers.xml b/test/fixtures/macros/xml-out/xml/structhandlers.xml index 88a75b7c..ef2efe8f 100644 --- a/test/fixtures/macros/xml-out/xml/structhandlers.xml +++ b/test/fixtures/macros/xml-out/xml/structhandlers.xml @@ -37,7 +37,7 @@ - + struct handlers * @@ -52,7 +52,7 @@ - + @@ -69,7 +69,7 @@ - + handlersfallback handlersretry diff --git a/test/snapshots/macros/api.md b/test/snapshots/macros/api.md index 3113d0fc..8e951378 100644 --- a/test/snapshots/macros/api.md +++ b/test/snapshots/macros/api.md @@ -80,7 +80,7 @@ Defined in src/macros.h:23 #define RETRY_HOOK registry->retry ``` -Defined in src/macros.h:51 +Defined in src/macros.h:72 Shorthand for the registry retry hook. Its initializer references another documented symbol, so Doxygen emits a cross-reference inside the initializer. @@ -102,6 +102,36 @@ Reset the retry counter. --- +{#drain} + +### drain + +```cpp +void drain(void) +``` + +Defined in src/macros.h:49 + +Drain the queue. + +Ordering rules: + +* first + * first.a + * first.b +* second + * second.a +* third + +Steps: + +1. open + 1. acquire + 1. verify +1. close + +--- + {#configure} ### configure @@ -110,7 +140,7 @@ Reset the retry counter. struct handlers * configure(struct handlers * target, int count) ``` -Defined in src/macros.h:60 +Defined in src/macros.h:81 Configure a handler registry. @@ -138,6 +168,6 @@ struct handlers * registry Type: struct [`handlers`](handlers.md#handlers) * -Defined in src/macros.h:45 +Defined in src/macros.h:66 Generated by [Moxygen](https://0state.com/moxygen) \ No newline at end of file diff --git a/test/snapshots/macros/handlers.md b/test/snapshots/macros/handlers.md index 5ac3fcba..eab6cc20 100644 --- a/test/snapshots/macros/handlers.md +++ b/test/snapshots/macros/handlers.md @@ -10,7 +10,7 @@ struct handlers ``` -Defined in src/macros.h:33 +Defined in src/macros.h:54 Registry of handler hooks. @@ -31,7 +31,7 @@ Registry of handler hooks. int(* retry)(int attempt, const char *reason) ``` -Defined in src/macros.h:41 +Defined in src/macros.h:62 Called when a retry is scheduled. @@ -57,7 +57,7 @@ struct handlers * fallback Type: struct [`handlers`](#handlers) * -Defined in src/macros.h:44 +Defined in src/macros.h:65 The registry consulted when this one has no handler. From bc8418f26a78827c8fbfb97144a2699c371d1f5e Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 1 Sep 2026 13:09:25 +1000 Subject: [PATCH 2/4] feat(markdown): add a github flavour for admonitions fixes #126 --- README.md | 1 + docs/cli-reference.md | 1 + src/cli.ts | 2 + src/index.ts | 1 + src/parser.ts | 46 ++++++++++---- src/types.ts | 2 + test/fixtures/macros/src/macros.h | 11 ++++ .../macros/xml-out/xml/deprecated.xml | 18 ++++++ test/fixtures/macros/xml-out/xml/index.xml | 3 + .../fixtures/macros/xml-out/xml/macros_8h.xml | 63 ++++++++++++++----- .../macros/xml-out/xml/structhandlers.xml | 6 +- test/integration.test.ts | 13 ++++ test/snapshots/macros/FILES.txt | 3 +- test/snapshots/macros/api.md | 44 +++++++++++-- test/snapshots/macros/handlers.md | 6 +- test/snapshots/macros/page-deprecated.md | 4 ++ 16 files changed, 185 insertions(+), 39 deletions(-) create mode 100644 test/fixtures/macros/xml-out/xml/deprecated.xml create mode 100644 test/snapshots/macros/page-deprecated.md diff --git a/README.md b/README.md index 55f01432..dc6c9ca2 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Options: -a, --anchors add anchors to internal links -H, --html-anchors add HTML anchors to internal links -l, --language programming language (default: cpp) + -F, --flavour markdown dialect for admonitions: pandoc or github (default: pandoc) -t, --templates custom templates directory -s, --source-root source root used to resolve file-level group membership diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 2bc2dfdf..f9ffaa00 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -21,6 +21,7 @@ moxygen [options] | `-a, --anchors` | Add anchors to internal links | | `-H, --html-anchors` | Add HTML anchors to internal links | | `-l, --language ` | Programming language (default: `cpp`) | +| `-F, --flavour ` | Markdown dialect for admonitions: `pandoc` or `github` (default: `pandoc`) | | `-t, --templates ` | Custom templates directory | | `-s, --source-root ` | Source root for file-level group resolution | | `-f, --frontmatter` | Prepend YAML frontmatter to output files | diff --git a/src/cli.ts b/src/cli.ts index 5ef5d33a..d4a06c90 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -23,6 +23,7 @@ program .option('-a, --anchors', 'add anchors to internal links', false) .option('-H, --html-anchors', 'add HTML anchors to internal links', false) .option('-l, --language ', 'programming language', 'cpp') + .option('-F, --flavour ', 'markdown dialect for admonitions: pandoc or github', 'pandoc') .option('-t, --templates ', 'custom templates directory') .option('-s, --source-root ', 'source root used to resolve file-level group membership') .option('-f, --frontmatter', 'prepend YAML frontmatter to output files', false) @@ -41,6 +42,7 @@ program anchors: opts.anchors as boolean, htmlAnchors: opts.htmlAnchors as boolean, language: opts.language as string, + flavour: opts.flavour as string, templates: opts.templates as string | undefined, sourceRoot: opts.sourceRoot as string | undefined, frontmatter: opts.frontmatter as boolean, diff --git a/src/index.ts b/src/index.ts index ea37b0d7..92603b97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,6 +61,7 @@ export const defaultOptions: MoxygenOptions = { anchors: true, htmlAnchors: false, language: 'cpp', + flavour: 'pandoc', templates: 'templates', titleSeparator: ' - ', quiet: false, diff --git a/src/parser.ts b/src/parser.ts index 2f578e43..fcf126d1 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -38,6 +38,34 @@ function currentContextName(context: XmlElement[]): string | undefined { const LIST_ELEMENTS = new Set(['orderedlist', 'itemizedlist', 'variablelist']); +/** Doxygen sections that read as a callout rather than as prose. */ +const ADMONITIONS: Record = { + note: { container: 'note', alert: 'NOTE' }, + remark: { container: 'note', alert: 'NOTE' }, + warning: { container: 'warning', alert: 'WARNING' }, + attention: { container: 'warning', alert: 'IMPORTANT' }, + deprecated: { container: 'warning', alert: 'CAUTION' }, +}; + +/** + * Render a callout in the configured dialect. Pandoc-style containers are the + * default; GitHub renders those as literal text and wants blockquote alerts, + * where every line of the body carries the quote marker. + */ +function renderAdmonition(kind: string, body: string, flavour: string): string { + const admonition = ADMONITIONS[kind]; + const prefixed = kind === 'deprecated' ? `**Deprecated.** ${body}` : body; + + if (flavour !== 'github') { + return `\n:::${admonition.container}\n${prefixed}\n:::\n\n`; + } + + const quoted = prefixed.trim().split('\n') + .map((line) => (line.trim() ? `> ${line}` : '>')) + .join('\n'); + return `\n> [!${admonition.alert}]\n${quoted}\n\n`; +} + function listMarker(name: string | undefined): string { return name === 'orderedlist' ? '1. ' : name === 'variablelist' ? ': ' : '* '; } @@ -170,12 +198,10 @@ function toMarkdown(element: unknown, context: XmlElement[] = [], options: Markd case 'simplesect': { const kind = el.$?.kind; context.push(el); - if (kind === 'attention' || kind === 'warning') { - s = '\n:::warning\n'; - } else if (kind === 'note' || kind === 'remark') { - s = '\n:::note\n'; - } else if (kind === 'deprecated') { - s = '\n:::warning\n**Deprecated.** '; + if (kind && ADMONITIONS[kind]) { + // The body is wrapped when the section closes, once its content is + // known: GitHub alerts have to prefix every line of it. + s = ''; } else if (kind === 'return') { s = '\n#### Returns\n'; } else if (kind === 'see') { @@ -273,11 +299,9 @@ function toMarkdown(element: unknown, context: XmlElement[] = [], options: Markd switch (el['#name']) { case 'simplesect': { const closeKind = el.$?.kind; - if (closeKind === 'attention' || closeKind === 'warning' || closeKind === 'note' || closeKind === 'remark' || closeKind === 'deprecated') { - s += '\n:::\n\n'; - } else { - s += '\n\n'; - } + s = closeKind && ADMONITIONS[closeKind] + ? renderAdmonition(closeKind, s, parserOptions?.flavour ?? 'pandoc') + : `${s}\n\n`; context.pop(); break; } diff --git a/src/types.ts b/src/types.ts index 860eeb00..19ae1fce 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,6 +29,8 @@ export interface MoxygenOptions { anchors: boolean; htmlAnchors: boolean; language: string; + /** Markdown dialect for admonitions: 'pandoc' containers or 'github' alerts. */ + flavour: string; templates: string; /** Separator joining qualified name, owner, signature and summary in search snippets. */ titleSeparator: string; diff --git a/test/fixtures/macros/src/macros.h b/test/fixtures/macros/src/macros.h index 1c53cfaa..eca86afb 100644 --- a/test/fixtures/macros/src/macros.h +++ b/test/fixtures/macros/src/macros.h @@ -27,6 +27,17 @@ */ void reset(int); +/** + Flush pending work. + + @note Flushing is best effort. + @remark Callers usually prefer drain(). + @warning Blocks until the queue is empty. + @attention Never call this from a signal handler. + @deprecated Use drain() instead. +*/ +void flush(void); + /** Drain the queue. diff --git a/test/fixtures/macros/xml-out/xml/deprecated.xml b/test/fixtures/macros/xml-out/xml/deprecated.xml new file mode 100644 index 00000000..5b2a5f99 --- /dev/null +++ b/test/fixtures/macros/xml-out/xml/deprecated.xml @@ -0,0 +1,18 @@ + + + + deprecated + Deprecated List + + + + +Member flush (void) +Use drain() instead. + + + + + + + diff --git a/test/fixtures/macros/xml-out/xml/index.xml b/test/fixtures/macros/xml-out/xml/index.xml index 4fe74883..c316eac2 100644 --- a/test/fixtures/macros/xml-out/xml/index.xml +++ b/test/fixtures/macros/xml-out/xml/index.xml @@ -12,9 +12,12 @@ RETRY_HOOK registry reset + flush drain configure + deprecated + src diff --git a/test/fixtures/macros/xml-out/xml/macros_8h.xml b/test/fixtures/macros/xml-out/xml/macros_8h.xml index 20d83b13..54143e08 100644 --- a/test/fixtures/macros/xml-out/xml/macros_8h.xml +++ b/test/fixtures/macros/xml-out/xml/macros_8h.xml @@ -91,7 +91,7 @@ - + @@ -106,7 +106,7 @@ - + @@ -127,6 +127,33 @@ + + void + void flush + (void) + flush + + void + + + + +Flush pending work. +Flushing is best effort. + +Callers usually prefer drain(). + +Blocks until the queue is empty. + +Never call this from a signal handler. + +DeprecatedUse drain() instead. + + + + + + void void drain @@ -166,7 +193,7 @@ - + struct handlers * @@ -208,7 +235,7 @@ - + @@ -229,19 +256,21 @@ voidreset(int); -voiddrain(void); - -structhandlers{ -int(*retry)(intattempt,constchar*reason); - -structhandlers*fallback; -}*registry; - -#defineRETRY_HOOKregistry->retry - -structhandlers*configure(structhandlers*target,intcount); - -#endif/*__MACROS_H__*/ +voidflush(void); + +voiddrain(void); + +structhandlers{ +int(*retry)(intattempt,constchar*reason); + +structhandlers*fallback; +}*registry; + +#defineRETRY_HOOKregistry->retry + +structhandlers*configure(structhandlers*target,intcount); + +#endif/*__MACROS_H__*/ diff --git a/test/fixtures/macros/xml-out/xml/structhandlers.xml b/test/fixtures/macros/xml-out/xml/structhandlers.xml index ef2efe8f..70cc66d5 100644 --- a/test/fixtures/macros/xml-out/xml/structhandlers.xml +++ b/test/fixtures/macros/xml-out/xml/structhandlers.xml @@ -37,7 +37,7 @@ - + struct handlers * @@ -52,7 +52,7 @@ - + @@ -69,7 +69,7 @@ - + handlersfallback handlersretry diff --git a/test/integration.test.ts b/test/integration.test.ts index 7abf87fa..a6b464e3 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -540,6 +540,19 @@ describe('integration', () => { expect(union).toContain('Integer representation.'); }); + it('renders admonitions as GitHub alerts when that flavour is chosen', async () => { + const output = join(outputRoot, 'macros-github', 'api.md'); + + await run({ directory: macrosXmlDir, output, anchors: true, quiet: true, flavour: 'github' }); + + const api = read(output); + expect(api).toContain('> [!NOTE]\n> Flushing is best effort.'); + expect(api).toContain('> [!WARNING]\n> Blocks until the queue is empty.'); + expect(api).toContain('> [!IMPORTANT]\n> Never call this from a signal handler.'); + // GitHub renders the Pandoc container syntax as literal text. + expect(api).not.toContain(':::'); + }); + it('renders macros by their real shape and keeps undocumented ones', async () => { const output = join(outputRoot, 'macros', 'api.md'); diff --git a/test/snapshots/macros/FILES.txt b/test/snapshots/macros/FILES.txt index b045d836..5e83d480 100644 --- a/test/snapshots/macros/FILES.txt +++ b/test/snapshots/macros/FILES.txt @@ -1,2 +1,3 @@ api.md -handlers.md \ No newline at end of file +handlers.md +page-deprecated.md \ No newline at end of file diff --git a/test/snapshots/macros/api.md b/test/snapshots/macros/api.md index 8e951378..dffddaef 100644 --- a/test/snapshots/macros/api.md +++ b/test/snapshots/macros/api.md @@ -80,7 +80,7 @@ Defined in src/macros.h:23 #define RETRY_HOOK registry->retry ``` -Defined in src/macros.h:72 +Defined in src/macros.h:83 Shorthand for the registry retry hook. Its initializer references another documented symbol, so Doxygen emits a cross-reference inside the initializer. @@ -102,6 +102,42 @@ Reset the retry counter. --- +{#flush} + +### flush + +```cpp +void flush(void) +``` + +Defined in src/macros.h:39 + +Flush pending work. + +:::note +Flushing is best effort. + +::: + +:::note +Callers usually prefer [drain()](#drain). + +::: + +:::warning +Blocks until the queue is empty. + +::: + +:::warning +Never call this from a signal handler. + +::: + +> Deprecated: Use [drain()](#drain) instead. + +--- + {#drain} ### drain @@ -110,7 +146,7 @@ Reset the retry counter. void drain(void) ``` -Defined in src/macros.h:49 +Defined in src/macros.h:60 Drain the queue. @@ -140,7 +176,7 @@ Steps: struct handlers * configure(struct handlers * target, int count) ``` -Defined in src/macros.h:81 +Defined in src/macros.h:92 Configure a handler registry. @@ -168,6 +204,6 @@ struct handlers * registry Type: struct [`handlers`](handlers.md#handlers) * -Defined in src/macros.h:66 +Defined in src/macros.h:77 Generated by [Moxygen](https://0state.com/moxygen) \ No newline at end of file diff --git a/test/snapshots/macros/handlers.md b/test/snapshots/macros/handlers.md index eab6cc20..1f8dc575 100644 --- a/test/snapshots/macros/handlers.md +++ b/test/snapshots/macros/handlers.md @@ -10,7 +10,7 @@ struct handlers ``` -Defined in src/macros.h:54 +Defined in src/macros.h:65 Registry of handler hooks. @@ -31,7 +31,7 @@ Registry of handler hooks. int(* retry)(int attempt, const char *reason) ``` -Defined in src/macros.h:62 +Defined in src/macros.h:73 Called when a retry is scheduled. @@ -57,7 +57,7 @@ struct handlers * fallback Type: struct [`handlers`](#handlers) * -Defined in src/macros.h:65 +Defined in src/macros.h:76 The registry consulted when this one has no handler. diff --git a/test/snapshots/macros/page-deprecated.md b/test/snapshots/macros/page-deprecated.md new file mode 100644 index 00000000..eaeb1916 --- /dev/null +++ b/test/snapshots/macros/page-deprecated.md @@ -0,0 +1,4 @@ +# Deprecated List {#deprecatedlist} + +* **Member [flush](api.md#flush) (void)**: {#deprecated_1_deprecated000001}Use [drain()](api.md#drain) instead. + From 541d32583b1616155339d00ba3c8f4ab3c166c0a Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 1 Sep 2026 13:23:31 +1000 Subject: [PATCH 3/4] fix(markdown): keep admonitions and definition lists inside their list --- src/cli.ts | 3 +- src/parser.ts | 30 +++++++---- src/templates.ts | 8 ++- test/fixtures/macros/src/macros.h | 13 +++++ .../fixtures/macros/xml-out/xml/macros_8h.xml | 52 +++++++++++++------ .../macros/xml-out/xml/structhandlers.xml | 6 +-- test/integration.test.ts | 3 ++ test/snapshots/macros/api.md | 22 ++++++-- test/snapshots/macros/handlers.md | 6 +-- 9 files changed, 100 insertions(+), 43 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index d4a06c90..de9d0cab 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -24,6 +24,7 @@ program .option('-H, --html-anchors', 'add HTML anchors to internal links', false) .option('-l, --language ', 'programming language', 'cpp') .option('-F, --flavour ', 'markdown dialect for admonitions: pandoc or github', 'pandoc') + .option('--flavor ', 'alias for --flavour') .option('-t, --templates ', 'custom templates directory') .option('-s, --source-root ', 'source root used to resolve file-level group membership') .option('-f, --frontmatter', 'prepend YAML frontmatter to output files', false) @@ -42,7 +43,7 @@ program anchors: opts.anchors as boolean, htmlAnchors: opts.htmlAnchors as boolean, language: opts.language as string, - flavour: opts.flavour as string, + flavour: (opts.flavor ?? opts.flavour) as string, templates: opts.templates as string | undefined, sourceRoot: opts.sourceRoot as string | undefined, frontmatter: opts.frontmatter as boolean, diff --git a/src/parser.ts b/src/parser.ts index fcf126d1..34e5245e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -52,18 +52,19 @@ const ADMONITIONS: Record = { * default; GitHub renders those as literal text and wants blockquote alerts, * where every line of the body carries the quote marker. */ -function renderAdmonition(kind: string, body: string, flavour: string): string { +function renderAdmonition(kind: string, body: string, flavour: string, indent: string): string { const admonition = ADMONITIONS[kind]; const prefixed = kind === 'deprecated' ? `**Deprecated.** ${body}` : body; - if (flavour !== 'github') { - return `\n:::${admonition.container}\n${prefixed}\n:::\n\n`; - } + const lines = flavour === 'github' + ? [`> [!${admonition.alert}]`, ...prefixed.trim().split('\n').map((line) => (line.trim() ? `> ${line}` : '>'))] + : [`:::${admonition.container}`, ...prefixed.split('\n'), ':::']; - const quoted = prefixed.trim().split('\n') - .map((line) => (line.trim() ? `> ${line}` : '>')) - .join('\n'); - return `\n> [!${admonition.alert}]\n${quoted}\n\n`; + // Inside a list item the whole block has to sit at the item's content + // column, or it ends the list instead of belonging to it, and it closes on a + // single newline so the next item follows on. + const block = lines.map((line) => (line ? indent + line : line)).join('\n'); + return `\n${block}\n${indent ? '' : '\n'}`; } function listMarker(name: string | undefined): string { @@ -74,6 +75,13 @@ function enclosingLists(context: XmlElement[]): XmlElement[] { return context.filter((el) => LIST_ELEMENTS.has(el['#name'] ?? '')); } +/** Column at which the content of the current list item continues. */ +function listContentIndent(context: XmlElement[]): string { + return enclosingLists(context) + .map((el) => ' '.repeat(listMarker(el['#name']).length)) + .join(''); +} + /** * Indent for a list item, which Markdown measures against the marker of the * item it nests under, so an ordered parent indents further than a bullet. @@ -178,7 +186,9 @@ function toMarkdown(element: unknown, context: XmlElement[] = [], options: Markd context.push(el); break; case 'varlistentry': - s = '\n* '; + // The entry before this one already ended its line, so starting with a + // newline here would put a blank line between every term. + s = listIndent(context) + '* '; break; case 'term': s = '**'; @@ -300,7 +310,7 @@ function toMarkdown(element: unknown, context: XmlElement[] = [], options: Markd case 'simplesect': { const closeKind = el.$?.kind; s = closeKind && ADMONITIONS[closeKind] - ? renderAdmonition(closeKind, s, parserOptions?.flavour ?? 'pandoc') + ? renderAdmonition(closeKind, s, parserOptions?.flavour ?? 'pandoc', listContentIndent(context)) : `${s}\n\n`; context.pop(); break; diff --git a/src/templates.ts b/src/templates.ts index f1f6a3dc..c57cf747 100644 --- a/src/templates.ts +++ b/src/templates.ts @@ -19,7 +19,8 @@ const DEFAULT_RENDER_CONTEXT: RenderContext = { headingBase: 1, }; -const MARKDOWN_LINK = /\[([^\]]+)\]\(([^)]+)\)/g; +const MARKDOWN_LINK = /\[([^\]]+)\]\(([^)]+)\)/; +const MARKDOWN_LINKS = /\[([^\]]+)\]\(([^)]+)\)/g; /** * Render a type for a table cell. @@ -34,11 +35,8 @@ function typeCell(type: string): string { const trimmed = (type || '').trim(); if (!trimmed) return ''; - MARKDOWN_LINK.lastIndex = 0; if (!MARKDOWN_LINK.test(trimmed)) return `\`${trimmed}\``; - - MARKDOWN_LINK.lastIndex = 0; - return trimmed.replace(MARKDOWN_LINK, '[`$1`]($2)'); + return trimmed.replace(MARKDOWN_LINKS, '[`$1`]($2)'); } function headingLevel(relativeLevel: unknown, context: RenderContext): number { diff --git a/test/fixtures/macros/src/macros.h b/test/fixtures/macros/src/macros.h index eca86afb..be9a26ce 100644 --- a/test/fixtures/macros/src/macros.h +++ b/test/fixtures/macros/src/macros.h @@ -56,6 +56,19 @@ void flush(void); 1. acquire 2. verify 2. close + + Terms: + +
+
pending
queued but not started
+
settled
finished either way
+
+ + Caveats: + + - the queue may grow while draining + @note callers should not rely on the count + - draining twice is safe */ void drain(void); diff --git a/test/fixtures/macros/xml-out/xml/macros_8h.xml b/test/fixtures/macros/xml-out/xml/macros_8h.xml index 54143e08..5aebc0a2 100644 --- a/test/fixtures/macros/xml-out/xml/macros_8h.xml +++ b/test/fixtures/macros/xml-out/xml/macros_8h.xml @@ -91,7 +91,7 @@ - + @@ -106,7 +106,7 @@ - + @@ -187,13 +187,31 @@ verify -close +close + +Terms: + +pending +queued but not started + +settled +finished either way + + + +Caveats: + +the queue may grow while draining callers should not rely on the count + + +draining twice is safe + - + struct handlers * @@ -235,7 +253,7 @@ - + @@ -258,19 +276,19 @@ voidflush(void); -voiddrain(void); - -structhandlers{ -int(*retry)(intattempt,constchar*reason); +voiddrain(void); -structhandlers*fallback; -}*registry; - -#defineRETRY_HOOKregistry->retry - -structhandlers*configure(structhandlers*target,intcount); - -#endif/*__MACROS_H__*/ +structhandlers{ +int(*retry)(intattempt,constchar*reason); + +structhandlers*fallback; +}*registry; + +#defineRETRY_HOOKregistry->retry + +structhandlers*configure(structhandlers*target,intcount); + +#endif/*__MACROS_H__*/ diff --git a/test/fixtures/macros/xml-out/xml/structhandlers.xml b/test/fixtures/macros/xml-out/xml/structhandlers.xml index 70cc66d5..4baac7d7 100644 --- a/test/fixtures/macros/xml-out/xml/structhandlers.xml +++ b/test/fixtures/macros/xml-out/xml/structhandlers.xml @@ -37,7 +37,7 @@ - + struct handlers * @@ -52,7 +52,7 @@ - + @@ -69,7 +69,7 @@ - + handlersfallback handlersretry diff --git a/test/integration.test.ts b/test/integration.test.ts index a6b464e3..9245a8e9 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -551,6 +551,9 @@ describe('integration', () => { expect(api).toContain('> [!IMPORTANT]\n> Never call this from a signal handler.'); // GitHub renders the Pandoc container syntax as literal text. expect(api).not.toContain(':::'); + // An alert inside a list item sits at the item's content column, or it + // ends the list instead of belonging to it. + expect(api).toContain(' > [!NOTE]\n > callers should not rely on the count'); }); it('renders macros by their real shape and keeps undocumented ones', async () => { diff --git a/test/snapshots/macros/api.md b/test/snapshots/macros/api.md index dffddaef..b3fa14bf 100644 --- a/test/snapshots/macros/api.md +++ b/test/snapshots/macros/api.md @@ -80,7 +80,7 @@ Defined in src/macros.h:23 #define RETRY_HOOK registry->retry ``` -Defined in src/macros.h:83 +Defined in src/macros.h:96 Shorthand for the registry retry hook. Its initializer references another documented symbol, so Doxygen emits a cross-reference inside the initializer. @@ -146,7 +146,7 @@ Never call this from a signal handler. void drain(void) ``` -Defined in src/macros.h:60 +Defined in src/macros.h:73 Drain the queue. @@ -166,6 +166,20 @@ Steps: 1. verify 1. close +Terms: + +* **pending**: queued but not started +* **settled**: finished either way + +Caveats: + +* the queue may grow while draining + :::note + callers should not rely on the count + + ::: +* draining twice is safe + --- {#configure} @@ -176,7 +190,7 @@ Steps: struct handlers * configure(struct handlers * target, int count) ``` -Defined in src/macros.h:92 +Defined in src/macros.h:105 Configure a handler registry. @@ -204,6 +218,6 @@ struct handlers * registry Type: struct [`handlers`](handlers.md#handlers) * -Defined in src/macros.h:77 +Defined in src/macros.h:90 Generated by [Moxygen](https://0state.com/moxygen) \ No newline at end of file diff --git a/test/snapshots/macros/handlers.md b/test/snapshots/macros/handlers.md index 1f8dc575..689c49c0 100644 --- a/test/snapshots/macros/handlers.md +++ b/test/snapshots/macros/handlers.md @@ -10,7 +10,7 @@ struct handlers ``` -Defined in src/macros.h:65 +Defined in src/macros.h:78 Registry of handler hooks. @@ -31,7 +31,7 @@ Registry of handler hooks. int(* retry)(int attempt, const char *reason) ``` -Defined in src/macros.h:73 +Defined in src/macros.h:86 Called when a retry is scheduled. @@ -57,7 +57,7 @@ struct handlers * fallback Type: struct [`handlers`](#handlers) * -Defined in src/macros.h:76 +Defined in src/macros.h:89 The registry consulted when this one has no handler. From 9082be085bb6a8f9227c7eddf26418feef49a544 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 1 Sep 2026 13:23:31 +1000 Subject: [PATCH 4/4] Bump version to 2.1.19 --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d922651..74f77ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ Notable changes per release. Older history is in the git log. +## 2.1.19 - 2026-09-01 + +### Added + +- `--flavour github` renders admonitions as GitHub alerts, so `@note` becomes `> [!NOTE]` rather than the Pandoc `:::note` container that GitHub shows as literal text. `@warning` maps to `[!WARNING]`, `@attention` to `[!IMPORTANT]`, `@remark` to `[!NOTE]`. The default stays `pandoc` with unchanged output, and `--flavor` is accepted too. Requested by @gkodinov in #126. + +### Fixed + +- Nested lists keep their nesting, and list items no longer have a blank line between them. Indentation is measured against the parent item's marker, so a list under `1. ` indents three spaces and one under `* ` indents two, which is what Markdown requires to nest. Reported by @gkodinov in #115. +- Definition lists no longer put a blank line between terms. +- An admonition inside a list item sits at the item's content column instead of ending the list. + ## 2.1.18 - 2026-09-01 ### Changed diff --git a/package-lock.json b/package-lock.json index 5b82b1fd..6869056f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "moxygen", - "version": "2.1.18", + "version": "2.1.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "moxygen", - "version": "2.1.18", + "version": "2.1.19", "license": "MIT", "dependencies": { "commander": "^15.0.0", diff --git a/package.json b/package.json index 0520f21a..7f64afd5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moxygen", - "version": "2.1.18", + "version": "2.1.19", "description": "Doxygen XML to Markdown converter", "type": "module", "main": "dist/index.js",