comark 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,8 +27,7 @@ export function extractReusableNodes(markdown, lastOutput) {
27
27
  return {
28
28
  remainingMarkdownStartLine,
29
29
  reusedNodes: lastOutput.nodes.slice(0, lastValidNodeIndex + 1),
30
- remainingMarkdown: '\n' // Add back the new line character which will be remove by the slice and join
31
- + markdown.split('\n').slice(remainingMarkdownStartLine + 1).join('\n') || '',
30
+ remainingMarkdown: markdown.split('\n').slice(remainingMarkdownStartLine).join('\n') || '',
32
31
  };
33
32
  }
34
33
  return {
@@ -24,16 +24,22 @@ const INLINE_TAG_MAP = {
24
24
  * Convert Markdown-It tokens to a Comark tree
25
25
  */
26
26
  export function marmdownItTokensToComarkTree(tokens, options = { startLine: 0, preservePositions: false }) {
27
+ const state = {
28
+ headingSlugCounts: new Map(),
29
+ preservePositions: options.preservePositions,
30
+ };
27
31
  const nodes = [];
28
32
  let i = 0;
29
33
  let endLine = options.startLine;
30
34
  while (i < tokens.length) {
31
- const result = processBlockToken(tokens, i, false);
35
+ const result = processBlockToken(tokens, i, false, state);
32
36
  if (result.node) {
33
37
  if (options.preservePositions) {
34
38
  for (let j = i; j < result.nextIndex; j++) {
35
39
  if (tokens[j].map && tokens[j].map[1]) {
36
- endLine = tokens[j].map[1] + options.startLine;
40
+ endLine = tokens[j].map[1]
41
+ + options.startLine
42
+ + (tokens[j].type?.endsWith('_close') ? 1 : 0);
37
43
  }
38
44
  }
39
45
  if (!result.node[1].$) {
@@ -207,7 +213,7 @@ function extractAttributes(tokens, startIndex, skipEmptyText = true) {
207
213
  }
208
214
  return { attrs: {}, nextIndex: startIndex };
209
215
  }
210
- function processBlockToken(tokens, startIndex, insideNestedContext = false) {
216
+ function processBlockToken(tokens, startIndex, insideNestedContext = false, state) {
211
217
  const token = tokens[startIndex];
212
218
  if (token.type === 'hr') {
213
219
  return { node: ['hr', {}], nextIndex: startIndex + 1 };
@@ -221,7 +227,7 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false) {
221
227
  const inner = content.endsWith('-->') ? content.slice(4, -3) : content.slice(4);
222
228
  return { node: [null, {}, inner], nextIndex: startIndex + 1 };
223
229
  }
224
- const children = processBlockChildren(tokens, startIndex + 1, 'html_block_close', false, false, false);
230
+ const children = processBlockChildren(tokens, startIndex + 1, 'html_block_close', false, false, false, state);
225
231
  const [node1] = htmlToComarkNodes(content);
226
232
  if (!node1) {
227
233
  return { node: null, nextIndex: startIndex + 1 };
@@ -234,7 +240,7 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false) {
234
240
  const componentName = token.tag || 'component';
235
241
  const attrs = processAttributes(token.attrs);
236
242
  // Process children until mdc_block_close, handling slots (#slotname)
237
- const children = processBlockChildrenWithSlots(tokens, startIndex + 1, 'mdc_block_close');
243
+ const children = processBlockChildrenWithSlots(tokens, startIndex + 1, 'mdc_block_close', state);
238
244
  // Return the component even if it has no children (empty component like ::component\n::)
239
245
  return { node: [componentName, attrs, ...children.nodes], nextIndex: children.nextIndex + 1 };
240
246
  }
@@ -299,11 +305,11 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false) {
299
305
  const level = token.tag.replace('h', '');
300
306
  const headingTag = `h${level}`;
301
307
  // Process heading children with inHeading flag for Comark component handling
302
- const children = processBlockChildren(tokens, startIndex + 1, 'heading_close', true, true, insideNestedContext);
308
+ const children = processBlockChildren(tokens, startIndex + 1, 'heading_close', true, true, insideNestedContext, state);
303
309
  if (children.nodes.length > 0) {
304
310
  // Always generate ID for all headings, no exceptions
305
311
  const textContent = extractTextContent(children.nodes);
306
- const headingId = slugify(textContent);
312
+ const headingId = uniqueSlug(slugify(textContent), state);
307
313
  // Always attach ID to the heading element itself
308
314
  return { node: [headingTag, { id: headingId }, ...children.nodes], nextIndex: children.nextIndex + 1 };
309
315
  }
@@ -312,7 +318,7 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false) {
312
318
  // Handle list items - paragraphs should be unwrapped
313
319
  if (token.type === 'list_item_open') {
314
320
  const attrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false });
315
- const children = processBlockChildren(tokens, startIndex + 1, 'list_item_close', false, false, true);
321
+ const children = processBlockChildren(tokens, startIndex + 1, 'list_item_close', false, false, true, state);
316
322
  // Unwrap paragraphs in list items
317
323
  const unwrapped = [];
318
324
  for (const child of children.nodes) {
@@ -334,46 +340,15 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false) {
334
340
  if (tagName) {
335
341
  const attrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false });
336
342
  const closeType = token.type.replace('_open', '_close');
337
- // Special handling for blockquotes
338
- if (tagName === 'blockquote') {
339
- // First pass: get children
340
- const children = processBlockChildren(tokens, startIndex + 1, closeType, false, false, false);
341
- // Rule: If a heading is the FIRST child AND there are additional children after it,
342
- // then the heading should NOT have an ID. Otherwise, headings should have IDs.
343
- if (children.nodes.length > 1) {
344
- const firstChild = children.nodes[0];
345
- // Check if first child is a heading (h1-h6)
346
- const isHeading = Array.isArray(firstChild)
347
- && typeof firstChild[0] === 'string'
348
- && /^h[1-6]$/.test(firstChild[0]);
349
- if (isHeading) {
350
- // Heading is first child with more siblings - reprocess without IDs
351
- const childrenNoIds = processBlockChildren(tokens, startIndex + 1, closeType, false, false, true);
352
- if (childrenNoIds.nodes.length > 0) {
353
- return { node: [tagName, attrs, ...childrenNoIds.nodes], nextIndex: childrenNoIds.nextIndex + 1 };
354
- }
355
- return { node: null, nextIndex: childrenNoIds.nextIndex + 1 };
356
- }
357
- }
358
- // All other cases: use original processing (allows IDs)
359
- if (children.nodes.length > 0) {
360
- return { node: [tagName, attrs, ...children.nodes], nextIndex: children.nextIndex + 1 };
361
- }
362
- return { node: null, nextIndex: children.nextIndex + 1 };
363
- }
364
- // For other elements (tables, etc.)
365
343
  const isNestedContext = ['td', 'th'].includes(tagName);
366
- const children = processBlockChildren(tokens, startIndex + 1, closeType, false, false, isNestedContext);
367
- if (children.nodes.length > 0) {
368
- return { node: [tagName, attrs, ...children.nodes], nextIndex: children.nextIndex + 1 };
369
- }
370
- return { node: null, nextIndex: children.nextIndex + 1 };
344
+ const children = processBlockChildren(tokens, startIndex + 1, closeType, false, false, isNestedContext, state);
345
+ return { node: [tagName, attrs, ...children.nodes], nextIndex: children.nextIndex + 1 };
371
346
  }
372
347
  const componentName = token.tag || 'component';
373
348
  const attrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false });
374
349
  return { node: [componentName, attrs], nextIndex: startIndex + 1 };
375
350
  }
376
- function processBlockChildrenWithSlots(tokens, startIndex, closeType) {
351
+ function processBlockChildrenWithSlots(tokens, startIndex, closeType, state) {
377
352
  const nodes = [];
378
353
  let i = startIndex;
379
354
  let currentSlotName = null;
@@ -420,7 +395,7 @@ function processBlockChildrenWithSlots(tokens, startIndex, closeType) {
420
395
  }
421
396
  // Process other block tokens
422
397
  // Comark components are not nested contexts - headings inside them should get IDs
423
- const result = processBlockToken(tokens, i, false);
398
+ const result = processBlockToken(tokens, i, false, state);
424
399
  i = result.nextIndex;
425
400
  if (result.node) {
426
401
  if (currentSlotName !== null) {
@@ -439,7 +414,7 @@ function processBlockChildrenWithSlots(tokens, startIndex, closeType) {
439
414
  }
440
415
  return { nodes, nextIndex: i };
441
416
  }
442
- function processBlockChildren(tokens, startIndex, closeType, inlineOnly, inHeading = false, insideNestedContext = false) {
417
+ function processBlockChildren(tokens, startIndex, closeType, inlineOnly, inHeading = false, insideNestedContext = false, state) {
443
418
  const nodes = [];
444
419
  let i = startIndex;
445
420
  while (i < tokens.length && tokens[i].type !== closeType) {
@@ -471,7 +446,7 @@ function processBlockChildren(tokens, startIndex, closeType, inlineOnly, inHeadi
471
446
  i++;
472
447
  }
473
448
  else {
474
- const result = processBlockToken(tokens, i, insideNestedContext);
449
+ const result = processBlockToken(tokens, i, insideNestedContext, state);
475
450
  i = result.nextIndex;
476
451
  if (result.node) {
477
452
  nodes.push(result.node);
@@ -544,6 +519,16 @@ function slugify(text) {
544
519
  }
545
520
  return slug;
546
521
  }
522
+ /**
523
+ * Return a unique slug by appending a numeric suffix for duplicates
524
+ */
525
+ function uniqueSlug(slug, state) {
526
+ if (!state)
527
+ return slug;
528
+ const count = state.headingSlugCounts.get(slug) ?? 0;
529
+ state.headingSlugCounts.set(slug, count + 1);
530
+ return count === 0 ? slug : `${slug}-${count}`;
531
+ }
547
532
  export function processInlineTokens(tokens, inHeading = false) {
548
533
  const nodes = [];
549
534
  let i = 0;
@@ -15,21 +15,10 @@ export function pre(node, state) {
15
15
  ? ' ' + attributes.meta
16
16
  : '';
17
17
  const result = '```' + language + filename + highlights + meta + '\n'
18
- + String(node[1]?.code || extractCode(node)).trim()
18
+ + String(node[1]?.code || textContent(node)).trim()
19
19
  + '\n```';
20
20
  return result + state.context.blockSeparator;
21
21
  }
22
- function extractCode(node) {
23
- const codeNode = node[2];
24
- if (Array.isArray(codeNode) && codeNode[0] === 'code') {
25
- const spans = codeNode.slice(2);
26
- const lineSpans = spans.filter(s => Array.isArray(s) && String(s[1]?.class ?? '').includes('line'));
27
- if (lineSpans.length > 0) {
28
- return lineSpans.map(span => textContent(span)).join('\n');
29
- }
30
- }
31
- return textContent(node);
32
- }
33
22
  function formatHighlights(highlights) {
34
23
  if (highlights.length === 0)
35
24
  return '';
package/dist/parse.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ComarkParseFn, ParseOptions, ComarkTree } from './types.ts';
2
2
  export { parseFrontmatter } from './internal/frontmatter.ts';
3
+ export { defineComarkPlugin } from './utils/helpers.ts';
3
4
  /**
4
5
  * Creates a parser function for Comark content.
5
6
  *
@@ -64,3 +65,19 @@ export declare function createParse(options?: ParseOptions): ComarkParseFn;
64
65
  * ```
65
66
  */
66
67
  export declare function parse(markdown: string, options?: ParseOptions): Promise<ComarkTree>;
68
+ /**
69
+ * Creates a serialized parser function for Comark content.
70
+ * This is useful for parsing large files in a streaming manner.
71
+ *
72
+ * @param options - Parser options
73
+ * @returns ComarkParseFn - The serialized parser function
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * import { createSerializedParse } from 'comark'
78
+ *
79
+ * const parse = createSerializedParse()
80
+ * const tree = await parse(content)
81
+ * console.log(tree.nodes)
82
+ */
83
+ export declare function createSerializedParse(options?: ParseOptions): ComarkParseFn;
package/dist/parse.js CHANGED
@@ -9,8 +9,11 @@ import { parseFrontmatter } from "./internal/frontmatter.js";
9
9
  import { extractReusableNodes } from "./internal/parse/incremental.js";
10
10
  import html_block from "./internal/parse/html/html_block_rule.js";
11
11
  import html_inline from "./internal/parse/html/html_inline_rule.js";
12
+ import { createSerializedTask } from "./utils/helpers.js";
12
13
  // Re-export frontmatter utilities
13
14
  export { parseFrontmatter } from "./internal/frontmatter.js";
15
+ // Re-export plugin utilities
16
+ export { defineComarkPlugin } from "./utils/helpers.js";
14
17
  /**
15
18
  * Creates a parser function for Comark content.
16
19
  *
@@ -161,3 +164,21 @@ export async function parse(markdown, options = {}) {
161
164
  const parse = createParse(options);
162
165
  return await parse(markdown);
163
166
  }
167
+ /**
168
+ * Creates a serialized parser function for Comark content.
169
+ * This is useful for parsing large files in a streaming manner.
170
+ *
171
+ * @param options - Parser options
172
+ * @returns ComarkParseFn - The serialized parser function
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * import { createSerializedParse } from 'comark'
177
+ *
178
+ * const parse = createSerializedParse()
179
+ * const tree = await parse(content)
180
+ * console.log(tree.nodes)
181
+ */
182
+ export function createSerializedParse(options = {}) {
183
+ return createSerializedTask(createParse(options));
184
+ }
@@ -1,2 +1,2 @@
1
- import type { ComarkPlugin } from 'comark';
2
- export default function alert(): ComarkPlugin;
1
+ declare const _default: import("comark").ComarkPluginFactory<unknown>;
2
+ export default _default;
@@ -1,4 +1,5 @@
1
1
  import { visit } from 'comark/utils';
2
+ import { defineComarkPlugin } from "../utils/helpers.js";
2
3
  const markers = {
3
4
  '!TIP': {
4
5
  type: 'tip',
@@ -26,41 +27,39 @@ const markers = {
26
27
  color: '#da3633',
27
28
  },
28
29
  };
29
- export default function alert() {
30
- return {
31
- name: 'alert',
32
- post(state) {
33
- visit(state.tree, node => Array.isArray(node) && node[0] === 'blockquote', (node) => {
34
- const element = node;
35
- if (node[2]?.[0] === 'span') {
36
- const content = String(node[2][2]).toUpperCase();
30
+ export default defineComarkPlugin(() => ({
31
+ name: 'alert',
32
+ post(state) {
33
+ visit(state.tree, node => Array.isArray(node) && node[0] === 'blockquote', (node) => {
34
+ const element = node;
35
+ if (node[2]?.[0] === 'span') {
36
+ const content = String(node[2][2]).toUpperCase();
37
+ const marker = markers[content];
38
+ if (marker) {
39
+ if (typeof node[3] === 'string') {
40
+ element[3] = String(element[3]).trimStart();
41
+ }
42
+ // remove span node
43
+ element.splice(2, 1);
44
+ element[1].as = marker.type;
45
+ }
46
+ }
47
+ else if (node[2]?.[0] === 'p') {
48
+ const paragraph = node[2];
49
+ if (paragraph[2]?.[0] === 'span') {
50
+ const content = String(paragraph[2][2]).toUpperCase();
37
51
  const marker = markers[content];
38
52
  if (marker) {
39
- if (typeof node[3] === 'string') {
40
- element[3] = String(element[3]).trimStart();
53
+ if (typeof paragraph[3] === 'string') {
54
+ paragraph[3] = String(paragraph[3]).trimStart();
41
55
  }
42
56
  // remove span node
43
- element.splice(2, 1);
57
+ paragraph.splice(2, 1);
58
+ // transform node
44
59
  element[1].as = marker.type;
45
60
  }
46
61
  }
47
- else if (node[2]?.[0] === 'p') {
48
- const paragraph = node[2];
49
- if (paragraph[2]?.[0] === 'span') {
50
- const content = String(paragraph[2][2]).toUpperCase();
51
- const marker = markers[content];
52
- if (marker) {
53
- if (typeof paragraph[3] === 'string') {
54
- paragraph[3] = String(paragraph[3]).trimStart();
55
- }
56
- // remove span node
57
- paragraph.splice(2, 1);
58
- // transform node
59
- element[1].as = marker.type;
60
- }
61
- }
62
- }
63
- });
64
- },
65
- };
66
- }
62
+ }
63
+ });
64
+ },
65
+ }));
@@ -1,3 +1,4 @@
1
- import type { ComarkPlugin, MarkdownItPlugin } from 'comark';
1
+ import type { MarkdownItPlugin } from 'comark';
2
2
  export declare const markdownItEmoji: MarkdownItPlugin;
3
- export default function comarkEmoji(): ComarkPlugin;
3
+ declare const _default: import("comark").ComarkPluginFactory<unknown>;
4
+ export default _default;
@@ -1,3 +1,4 @@
1
+ import { defineComarkPlugin } from "../utils/helpers.js";
1
2
  // Common emoji definitions (200+ emojis)
2
3
  // Organized by category for easier maintenance
3
4
  const EMOJI_MAP = new Map([
@@ -430,9 +431,7 @@ const emojiRule = (state, silent) => {
430
431
  export const markdownItEmoji = (md) => {
431
432
  md.inline.ruler.before('emphasis', 'emoji', emojiRule);
432
433
  };
433
- export default function comarkEmoji() {
434
- return {
435
- name: 'emoji',
436
- markdownItPlugins: [markdownItEmoji],
437
- };
438
- }
434
+ export default defineComarkPlugin(() => ({
435
+ name: 'emoji',
436
+ markdownItPlugins: [markdownItEmoji],
437
+ }));
@@ -1,4 +1,3 @@
1
- import type { ComarkPlugin } from 'comark';
2
1
  export interface HeadingsOptions {
3
2
  /**
4
3
  * Tag to extract as title and set to `tree.meta.title`.
@@ -13,7 +12,7 @@ export interface HeadingsOptions {
13
12
  descriptionTag?: string;
14
13
  /**
15
14
  * Whether to remove the extracted nodes from the tree.
16
- * @default true
15
+ * @default false
17
16
  */
18
17
  remove?: boolean;
19
18
  }
@@ -45,4 +44,5 @@ export interface HeadingsOptions {
45
44
  * headings({ remove: false })
46
45
  * ```
47
46
  */
48
- export default function headings(options?: HeadingsOptions): ComarkPlugin;
47
+ declare const _default: import("comark").ComarkPluginFactory<HeadingsOptions>;
48
+ export default _default;
@@ -1,3 +1,4 @@
1
+ import { defineComarkPlugin } from "../utils/helpers.js";
1
2
  function getTag(node) {
2
3
  if (Array.isArray(node) && node.length >= 1) {
3
4
  return node[0];
@@ -49,8 +50,8 @@ function flattenNodeText(node) {
49
50
  * headings({ remove: false })
50
51
  * ```
51
52
  */
52
- export default function headings(options = {}) {
53
- const { titleTag = 'h1', descriptionTag = 'p', remove = true } = options;
53
+ export default defineComarkPlugin((options = {}) => {
54
+ const { titleTag = 'h1', descriptionTag = 'p', remove = false } = options;
54
55
  return {
55
56
  name: 'headings',
56
57
  post(state) {
@@ -82,4 +83,4 @@ export default function headings(options = {}) {
82
83
  }
83
84
  },
84
85
  };
85
- }
86
+ });
@@ -1,7 +1,5 @@
1
- import type { LanguageRegistration } from 'shiki';
2
- import type { ComarkNode, ComarkTree, ComarkPlugin } from 'comark';
3
- import type { ShikiPrimitive, ThemeRegistration } from '@shikijs/primitive';
4
- import type { ShikiTransformer } from '@shikijs/types';
1
+ import type { LanguageRegistration, ShikiTransformer, ShikiInternal, ThemeRegistration } from 'shiki';
2
+ import type { ComarkNode, ComarkTree } from 'comark';
5
3
  export interface HighlightOptions {
6
4
  /**
7
5
  * Whether to use the default language definitions
@@ -47,7 +45,7 @@ export interface CodeBlockAttributes {
47
45
  * Get or create the Shiki highlighter instance
48
46
  * Uses a singleton pattern to avoid creating multiple highlighters
49
47
  */
50
- export declare function getHighlighter(options?: HighlightOptions): Promise<ShikiPrimitive>;
48
+ export declare function getHighlighter(options?: HighlightOptions): Promise<ShikiInternal>;
51
49
  /**
52
50
  * Highlight code using Shiki with codeToTokens
53
51
  * Returns comark nodes built from hast
@@ -68,4 +66,5 @@ export declare function highlightCodeBlocks(tree: ComarkTree, options?: Highligh
68
66
  * Useful for testing or when you want to reconfigure
69
67
  */
70
68
  export declare function resetHighlighter(): void;
71
- export default function highlight(options?: HighlightOptions): ComarkPlugin;
69
+ declare const _default: import("comark").ComarkPluginFactory<HighlightOptions>;
70
+ export default _default;
@@ -1,4 +1,5 @@
1
- import { createShikiPrimitive } from '@shikijs/primitive';
1
+ import { defineComarkPlugin } from "../utils/helpers.js";
2
+ import { createShikiInternal } from 'shiki';
2
3
  import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
3
4
  import { codeToHast } from 'shiki/core';
4
5
  let highlighter = null;
@@ -23,7 +24,7 @@ export async function getHighlighter(options = {}) {
23
24
  try {
24
25
  highlighterPromise = (async () => {
25
26
  const { themes, languages } = await registerDefaults(options);
26
- const hl = createShikiPrimitive({
27
+ const hl = await createShikiInternal({
27
28
  themes: themes,
28
29
  langs: languages,
29
30
  langAlias: {
@@ -51,10 +52,10 @@ async function registerDefaults(options) {
51
52
  const languages = options.languages || [];
52
53
  const promises = [];
53
54
  if (options.registerDefaultThemes !== false) {
54
- promises.push(import('@shikijs/themes/material-theme-lighter').then(m => ({ type: 'theme', value: m.default })), import('@shikijs/themes/material-theme-palenight').then(m => ({ type: 'theme', value: m.default })));
55
+ promises.push(import('shiki/dist/themes/material-theme-lighter.mjs').then(m => ({ type: 'theme', value: m.default })), import('shiki/dist/themes/material-theme-palenight.mjs').then(m => ({ type: 'theme', value: m.default })));
55
56
  }
56
57
  if (options.registerDefaultLanguages !== false) {
57
- promises.push(import('@shikijs/langs/vue').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/tsx').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/svelte').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/typescript').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/javascript').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/mdc').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/bash').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/json').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/yaml').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/astro').then(m => ({ type: 'lang', value: m.default })));
58
+ promises.push(import('shiki/dist/langs/vue.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/tsx.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/svelte.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/typescript.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/javascript.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/mdc.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/bash.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/json.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/yaml.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/astro.mjs').then(m => ({ type: 'lang', value: m.default })));
58
59
  }
59
60
  const results = await Promise.all(promises);
60
61
  for (const result of results) {
@@ -163,6 +164,7 @@ export async function highlightCodeBlocks(tree, options = {}) {
163
164
  const newNodes = JSON.parse(JSON.stringify(tree.nodes));
164
165
  for (let i = 0; i < codeBlocks.length; i++) {
165
166
  const { node, path } = codeBlocks[i];
167
+ const preAttrs = node[1];
166
168
  const result = highlightedResults[i];
167
169
  const preNode = result.nodes[0];
168
170
  const preNodeClasses = typeof preNode === 'string'
@@ -171,8 +173,26 @@ export async function highlightCodeBlocks(tree, options = {}) {
171
173
  ? preNode[1].class
172
174
  : String(preNode[1].class).split(' '));
173
175
  const codeChildren = preNode[2].slice(2);
174
- const children = typeof preNode === 'string' ? preNode : codeChildren.filter(element => element !== '\n');
175
- const preAttrs = node[1];
176
+ const children = typeof preNode === 'string'
177
+ ? preNode
178
+ : codeChildren;
179
+ if (Array.isArray(children)) {
180
+ let line = 1;
181
+ for (const child of children) {
182
+ if (Array.isArray(child)) {
183
+ if (Array.isArray(preAttrs.highlights) && preAttrs.highlights.includes(line)) {
184
+ child[1].class = `${child[1].class ?? ''} highlight`.trim();
185
+ // TODO: (enforcing default style) once we unify all ecosystem styles we can remove this
186
+ child[1].style = 'display: inline-block';
187
+ }
188
+ else {
189
+ // TODO: (enforcing default style) once we unify all ecosystem styles we can remove this
190
+ child[1].style = 'display: inline';
191
+ }
192
+ line += 1;
193
+ }
194
+ }
195
+ }
176
196
  const newPreAttrs = {
177
197
  ...preAttrs,
178
198
  class: [...preNodeClasses, options.themes?.dark?.name ? `dark:${options.themes?.dark?.name}` : ''].filter(Boolean).join(' '),
@@ -224,11 +244,9 @@ export function resetHighlighter() {
224
244
  highlighterPromise = null;
225
245
  loadedThemes.clear();
226
246
  }
227
- export default function highlight(options = {}) {
228
- return {
229
- name: 'highlight',
230
- async post(state) {
231
- state.tree = await highlightCodeBlocks(state.tree, options);
232
- },
233
- };
234
- }
247
+ export default defineComarkPlugin((options = {}) => ({
248
+ name: 'highlight',
249
+ async post(state) {
250
+ state.tree = await highlightCodeBlocks(state.tree, options);
251
+ },
252
+ }));
@@ -1,4 +1,3 @@
1
- import type { ComarkPlugin } from 'comark';
2
1
  export interface MathConfig {
3
2
  /**
4
3
  * Throw on parse errors or return error message
@@ -56,4 +55,5 @@ export declare function validateMath(code: string): boolean;
56
55
  * })
57
56
  * ```
58
57
  */
59
- export default function math(config?: MathConfig): ComarkPlugin;
58
+ declare const _default: import("comark").ComarkPluginFactory<MathConfig>;
59
+ export default _default;
@@ -1,4 +1,5 @@
1
1
  import katex from 'katex';
2
+ import { defineComarkPlugin } from "../utils/helpers.js";
2
3
  /**
3
4
  * Render LaTeX math expression to HTML using KaTeX
4
5
  *
@@ -253,11 +254,9 @@ function markdownItMath(md, config = {}) {
253
254
  * })
254
255
  * ```
255
256
  */
256
- export default function math(config) {
257
- return {
258
- name: 'math',
259
- markdownItPlugins: [
260
- ((md) => markdownItMath(md, config ?? {})),
261
- ],
262
- };
263
- }
257
+ export default defineComarkPlugin((config = {}) => ({
258
+ name: 'math',
259
+ markdownItPlugins: [
260
+ ((md) => markdownItMath(md, config)),
261
+ ],
262
+ }));
@@ -1,4 +1,3 @@
1
- import type { ComarkPlugin } from 'comark';
2
1
  export type ThemeNames = 'zinc-light' | 'zinc-dark' | 'tokyo-night' | 'tokyo-night-storm' | 'tokyo-night-light' | 'catppuccin-mocha' | 'catppuccin-latte' | 'nord' | 'nord-light' | 'dracula' | 'github-light' | 'github-dark' | 'solarized-light' | 'solarized-dark' | 'one-dark';
3
2
  export interface MermaidConfig {
4
3
  /**
@@ -35,4 +34,5 @@ export declare function searchProps(content: string, index?: number): {
35
34
  * })
36
35
  * ```
37
36
  */
38
- export default function comarkMermaid(config?: MermaidConfig): ComarkPlugin;
37
+ declare const _default: import("comark").ComarkPluginFactory<MermaidConfig>;
38
+ export default _default;
@@ -1,3 +1,4 @@
1
+ import { defineComarkPlugin } from "../utils/helpers.js";
1
2
  /**
2
3
  * markdown-it plugin for mermaid diagrams
3
4
  * This handles ```mermaid code blocks
@@ -175,11 +176,9 @@ export function searchProps(content, index = 0) {
175
176
  * })
176
177
  * ```
177
178
  */
178
- export default function comarkMermaid(config) {
179
- return {
180
- name: 'mermaid',
181
- markdownItPlugins: [
182
- ((md) => markdownItMermaid(md, config)),
183
- ],
184
- };
185
- }
179
+ export default defineComarkPlugin((config = {}) => ({
180
+ name: 'mermaid',
181
+ markdownItPlugins: [
182
+ ((md) => markdownItMermaid(md, config)),
183
+ ],
184
+ }));
@@ -1,5 +1,4 @@
1
- import type { ComarkPlugin } from 'comark';
2
- import type { PropsValidationOptions } from '../internal/props-validation';
1
+ import type { PropsValidationOptions } from '../internal/props-validation.ts';
3
2
  interface SecurityOptions extends PropsValidationOptions {
4
3
  /**
5
4
  * Tags to remove entirely from the output tree.
@@ -7,5 +6,5 @@ interface SecurityOptions extends PropsValidationOptions {
7
6
  */
8
7
  blockedTags?: string[];
9
8
  }
10
- export default function security(options?: SecurityOptions): ComarkPlugin;
11
- export {};
9
+ declare const _default: import("comark").ComarkPluginFactory<SecurityOptions>;
10
+ export default _default;
@@ -1,6 +1,7 @@
1
+ import { defineComarkPlugin } from "../utils/helpers.js";
1
2
  import { visit } from 'comark/utils';
2
- import { validateProps } from '../internal/props-validation';
3
- export default function security(options = {}) {
3
+ import { validateProps } from "../internal/props-validation.js";
4
+ export default defineComarkPlugin((options = {}) => {
4
5
  const { blockedTags = [], allowedLinkPrefixes, allowedImagePrefixes, allowedProtocols, defaultOrigin, allowDataImages, } = options;
5
6
  const dropSet = new Set(blockedTags.map(t => t.toLowerCase()));
6
7
  const propsOptions = {
@@ -29,4 +30,4 @@ export default function security(options = {}) {
29
30
  });
30
31
  },
31
32
  };
32
- }
33
+ });
@@ -1,2 +1,4 @@
1
- import type { ComarkPlugin } from '../types';
2
- export default function summary(delimiter?: string): ComarkPlugin;
1
+ declare const _default: import("comark").ComarkPluginFactory<{
2
+ delimiter?: string;
3
+ }>;
4
+ export default _default;
@@ -1,6 +1,8 @@
1
- import { applyAutoUnwrap } from '../internal/parse/auto-unwrap';
2
- import { marmdownItTokensToComarkTree } from '../internal/parse/token-processor';
3
- export default function summary(delimiter = '<!-- more -->') {
1
+ import { applyAutoUnwrap } from "../internal/parse/auto-unwrap.js";
2
+ import { marmdownItTokensToComarkTree } from "../internal/parse/token-processor.js";
3
+ import { defineComarkPlugin } from "../utils/helpers.js";
4
+ export default defineComarkPlugin((options = {}) => {
5
+ const { delimiter = '<!-- more -->' } = options;
4
6
  return {
5
7
  name: 'summary',
6
8
  post(state) {
@@ -19,4 +21,4 @@ export default function summary(delimiter = '<!-- more -->') {
19
21
  }
20
22
  },
21
23
  };
22
- }
24
+ });
@@ -4,5 +4,5 @@
4
4
  * This plugin runs before inline parsing to prevent Comark from interpreting
5
5
  * task list markers [X] and [ ] as Comark inline span syntax.
6
6
  */
7
- import type { ComarkPlugin } from 'comark';
8
- export default function comarkTaskList(): ComarkPlugin;
7
+ declare const _default: import("../types.ts").ComarkPluginFactory<unknown>;
8
+ export default _default;
@@ -4,6 +4,7 @@
4
4
  * This plugin runs before inline parsing to prevent Comark from interpreting
5
5
  * task list markers [X] and [ ] as Comark inline span syntax.
6
6
  */
7
+ import { defineComarkPlugin } from "../utils/helpers.js";
7
8
  function attrSet(token, name, value) {
8
9
  const index = token.attrIndex(name);
9
10
  const attr = [name, value];
@@ -109,9 +110,7 @@ function markdownItTaskList(md, options) {
109
110
  }
110
111
  });
111
112
  }
112
- export default function comarkTaskList() {
113
- return {
114
- name: 'task-list',
115
- markdownItPlugins: [markdownItTaskList],
116
- };
117
- }
113
+ export default defineComarkPlugin(() => ({
114
+ name: 'task-list',
115
+ markdownItPlugins: [markdownItTaskList],
116
+ }));
@@ -1,4 +1,4 @@
1
- import type { ComarkPlugin, ComarkTree } from 'comark';
1
+ import type { ComarkTree } from 'comark';
2
2
  export interface TocLink {
3
3
  id: string;
4
4
  text: string;
@@ -12,4 +12,5 @@ export interface Toc {
12
12
  links: TocLink[];
13
13
  }
14
14
  export declare function generateFlatToc(body: ComarkTree, options: Toc): Toc;
15
- export default function toc(options?: Partial<Toc>): ComarkPlugin;
15
+ declare const _default: import("comark").ComarkPluginFactory<Partial<Toc>>;
16
+ export default _default;
@@ -1,3 +1,4 @@
1
+ import { defineComarkPlugin } from "../utils/helpers.js";
1
2
  const TOC_TAGS = ['h2', 'h3', 'h4', 'h5', 'h6'];
2
3
  const TOC_TAGS_DEPTH = TOC_TAGS.reduce((tags, tag) => {
3
4
  tags[tag] = Number(tag.charAt(tag.length - 1));
@@ -105,7 +106,7 @@ export function generateFlatToc(body, options) {
105
106
  links,
106
107
  };
107
108
  }
108
- export default function toc(options = {}) {
109
+ export default defineComarkPlugin((options = {}) => {
109
110
  const { title = '', depth = 2, searchDepth = 2, links = [] } = options;
110
111
  return {
111
112
  name: 'toc',
@@ -115,4 +116,4 @@ export default function toc(options = {}) {
115
116
  state.tree.meta.toc = toc;
116
117
  },
117
118
  };
118
- }
119
+ });
package/dist/types.d.ts CHANGED
@@ -11,7 +11,7 @@ export type ComarkText = string;
11
11
  * @param {} - The attributes of the comment
12
12
  * @param string - The content of the comment
13
13
  */
14
- export type ComarkComment = [null, {}, string];
14
+ export type ComarkComment = [null, ComarkElementAttributes, string];
15
15
  /**
16
16
  * The Comark element attributes
17
17
  * @param [key: string]: unknown - The attributes of the element
@@ -190,6 +190,7 @@ export type ComarkPlugin = {
190
190
  pre?: (state: ComarkParsePreState) => Promise<void> | void;
191
191
  post?: (state: ComarkParsePostState) => Promise<void> | void;
192
192
  };
193
+ export type ComarkPluginFactory<Options> = (opts?: Options) => ComarkPlugin;
193
194
  export type ComponentManifest = (name: string) => Promise<unknown> | undefined | null;
194
195
  export interface ComarkContextProvider {
195
196
  components: Record<string, any>;
@@ -0,0 +1,12 @@
1
+ import type { ComarkPluginFactory } from '../types.ts';
2
+ /**
3
+ * Returns a function that invokes `fn` **strictly one at a time**: each call waits until the
4
+ * previous invocation has settled (resolved or rejected) before starting the next.
5
+ */
6
+ export declare function createSerializedTask<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => Promise<TResult>): (...args: TArgs) => Promise<TResult>;
7
+ /**
8
+ * Define a Comark plugin
9
+ * @param fn - The plugin factory function
10
+ * @returns The defined plugin
11
+ */
12
+ export declare function defineComarkPlugin<Options>(fn: ComarkPluginFactory<Options>): ComarkPluginFactory<Options>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Returns a function that invokes `fn` **strictly one at a time**: each call waits until the
3
+ * previous invocation has settled (resolved or rejected) before starting the next.
4
+ */
5
+ export function createSerializedTask(fn) {
6
+ let chain = Promise.resolve(null);
7
+ return (...args) => {
8
+ chain = chain
9
+ .then(() => fn(...args))
10
+ .catch(() => null);
11
+ return chain;
12
+ };
13
+ }
14
+ // #region define plugin
15
+ /**
16
+ * Define a Comark plugin
17
+ * @param fn - The plugin factory function
18
+ * @returns The defined plugin
19
+ */
20
+ export function defineComarkPlugin(fn) {
21
+ return fn;
22
+ }
23
+ // #endregion
@@ -0,0 +1 @@
1
+ export * from '../../src/utils/serialized-task'
@@ -0,0 +1 @@
1
+ export * from '../../src/utils/serialized-task.ts'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "comark",
3
3
  "type": "module",
4
- "version": "0.1.1",
4
+ "version": "0.2.0",
5
5
  "description": "Components in Markdown (Comark) parser with streaming support for Vue, React, Svelte and HTML",
6
6
  "author": "",
7
7
  "license": "MIT",
@@ -35,7 +35,6 @@
35
35
  "build": "tsc",
36
36
  "dev": "tsc --watch",
37
37
  "test": "vitest run",
38
- "benchmark": "node --import tsx benchmark.ts",
39
38
  "prepack": "tsc",
40
39
  "release": "release-it",
41
40
  "release:dry": "release-it --dry-run"
@@ -61,9 +60,9 @@
61
60
  "@shikijs/primitive": "^4.0.2",
62
61
  "@shikijs/twoslash": "^4.0.2",
63
62
  "@types/js-yaml": "^4.0.9",
63
+ "@types/markdown-it": "^14.1.2",
64
64
  "github-slugger": "^2.0.0",
65
65
  "hast-util-to-string": "^3.0.1",
66
- "markdown-it": "^14.1.1",
67
66
  "minimark": "0.2.0",
68
67
  "mitata": "^1.0.34",
69
68
  "tsx": "^4.21.0",