react-native-nitro-markdown 0.5.3 → 0.5.5

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.
Files changed (125) hide show
  1. package/README.md +89 -10
  2. package/android/CMakeLists.txt +1 -1
  3. package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +9 -3
  4. package/cpp/CMakeLists.txt +2 -1
  5. package/cpp/bindings/HybridMarkdownParser.cpp +4 -2
  6. package/cpp/core/MD4CParser.cpp +69 -2
  7. package/cpp/core/MD4CParser.hpp +17 -0
  8. package/cpp/core/MarkdownTypes.hpp +1 -1
  9. package/lib/commonjs/headless.js +2 -2
  10. package/lib/commonjs/markdown.js +56 -44
  11. package/lib/commonjs/markdown.js.map +1 -1
  12. package/lib/commonjs/renderers/blockquote.js +15 -13
  13. package/lib/commonjs/renderers/blockquote.js.map +1 -1
  14. package/lib/commonjs/renderers/code.js +57 -53
  15. package/lib/commonjs/renderers/code.js.map +1 -1
  16. package/lib/commonjs/renderers/heading.js +48 -46
  17. package/lib/commonjs/renderers/heading.js.map +1 -1
  18. package/lib/commonjs/renderers/horizontal-rule.js +10 -8
  19. package/lib/commonjs/renderers/horizontal-rule.js.map +1 -1
  20. package/lib/commonjs/renderers/image.js +12 -3
  21. package/lib/commonjs/renderers/image.js.map +1 -1
  22. package/lib/commonjs/renderers/list.js +75 -70
  23. package/lib/commonjs/renderers/list.js.map +1 -1
  24. package/lib/commonjs/renderers/math.js +4 -3
  25. package/lib/commonjs/renderers/math.js.map +1 -1
  26. package/lib/commonjs/renderers/paragraph.js +15 -13
  27. package/lib/commonjs/renderers/paragraph.js.map +1 -1
  28. package/lib/commonjs/renderers/style-cache.js +14 -0
  29. package/lib/commonjs/renderers/style-cache.js.map +1 -0
  30. package/lib/commonjs/renderers/table/index.js +7 -4
  31. package/lib/commonjs/renderers/table/index.js.map +1 -1
  32. package/lib/module/headless.js +2 -2
  33. package/lib/module/markdown.js +56 -44
  34. package/lib/module/markdown.js.map +1 -1
  35. package/lib/module/renderers/blockquote.js +15 -13
  36. package/lib/module/renderers/blockquote.js.map +1 -1
  37. package/lib/module/renderers/code.js +57 -53
  38. package/lib/module/renderers/code.js.map +1 -1
  39. package/lib/module/renderers/heading.js +48 -46
  40. package/lib/module/renderers/heading.js.map +1 -1
  41. package/lib/module/renderers/horizontal-rule.js +10 -8
  42. package/lib/module/renderers/horizontal-rule.js.map +1 -1
  43. package/lib/module/renderers/image.js +13 -4
  44. package/lib/module/renderers/image.js.map +1 -1
  45. package/lib/module/renderers/list.js +75 -70
  46. package/lib/module/renderers/list.js.map +1 -1
  47. package/lib/module/renderers/math.js +4 -3
  48. package/lib/module/renderers/math.js.map +1 -1
  49. package/lib/module/renderers/paragraph.js +15 -13
  50. package/lib/module/renderers/paragraph.js.map +1 -1
  51. package/lib/module/renderers/style-cache.js +10 -0
  52. package/lib/module/renderers/style-cache.js.map +1 -0
  53. package/lib/module/renderers/table/index.js +7 -4
  54. package/lib/module/renderers/table/index.js.map +1 -1
  55. package/lib/typescript/commonjs/Markdown.nitro.d.ts +1 -0
  56. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/headless.d.ts +2 -2
  58. package/lib/typescript/commonjs/markdown.d.ts +7 -1
  59. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/renderers/blockquote.d.ts +1 -1
  61. package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/renderers/heading.d.ts +1 -1
  64. package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +1 -1
  66. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/renderers/list.d.ts +1 -1
  69. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/renderers/math.d.ts +1 -1
  71. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/renderers/paragraph.d.ts +1 -1
  73. package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/renderers/style-cache.d.ts +3 -0
  75. package/lib/typescript/commonjs/renderers/style-cache.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/renderers/table/index.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/theme.d.ts +2 -2
  78. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  79. package/lib/typescript/module/Markdown.nitro.d.ts +1 -0
  80. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  81. package/lib/typescript/module/headless.d.ts +2 -2
  82. package/lib/typescript/module/markdown.d.ts +7 -1
  83. package/lib/typescript/module/markdown.d.ts.map +1 -1
  84. package/lib/typescript/module/renderers/blockquote.d.ts +1 -1
  85. package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
  86. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  87. package/lib/typescript/module/renderers/heading.d.ts +1 -1
  88. package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
  89. package/lib/typescript/module/renderers/horizontal-rule.d.ts +1 -1
  90. package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
  91. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  92. package/lib/typescript/module/renderers/list.d.ts +1 -1
  93. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  94. package/lib/typescript/module/renderers/math.d.ts +1 -1
  95. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  96. package/lib/typescript/module/renderers/paragraph.d.ts +1 -1
  97. package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
  98. package/lib/typescript/module/renderers/style-cache.d.ts +3 -0
  99. package/lib/typescript/module/renderers/style-cache.d.ts.map +1 -0
  100. package/lib/typescript/module/renderers/table/index.d.ts.map +1 -1
  101. package/lib/typescript/module/theme.d.ts +2 -2
  102. package/lib/typescript/module/theme.d.ts.map +1 -1
  103. package/nitro.json +12 -3
  104. package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -2
  105. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
  106. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +2 -2
  107. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +2 -2
  108. package/nitrogen/generated/ios/NitroMarkdown+autolinking.rb +2 -0
  109. package/nitrogen/generated/shared/c++/ParserOptions.hpp +6 -2
  110. package/package.json +6 -5
  111. package/react-native-nitro-markdown.podspec +3 -0
  112. package/src/Markdown.nitro.ts +1 -0
  113. package/src/headless.ts +2 -2
  114. package/src/markdown.tsx +102 -58
  115. package/src/renderers/blockquote.tsx +22 -17
  116. package/src/renderers/code.tsx +75 -63
  117. package/src/renderers/heading.tsx +60 -54
  118. package/src/renderers/horizontal-rule.tsx +17 -12
  119. package/src/renderers/image.tsx +15 -4
  120. package/src/renderers/list.tsx +93 -76
  121. package/src/renderers/math.tsx +8 -3
  122. package/src/renderers/paragraph.tsx +22 -17
  123. package/src/renderers/style-cache.ts +14 -0
  124. package/src/renderers/table/index.tsx +15 -10
  125. package/src/theme.ts +2 -2
package/README.md CHANGED
@@ -12,6 +12,9 @@ Native Markdown parsing and rendering for React Native, powered by [md4c](https:
12
12
  - **Full rendering pipeline** -- parser + renderer + streaming session in one package
13
13
  - **GFM support** -- tables, strikethrough, task lists, autolinks
14
14
  - **Math rendering** -- inline and block LaTeX via `react-native-mathjax-svg` (optional)
15
+ - **Opt-in HTML AST nodes** -- preserve raw HTML as `html_inline` / `html_block` for custom renderers
16
+ - **Large document support** -- top-level block virtualization and cached renderer styles
17
+ - **Code highlighting** -- built-in JS/TS, Python, and Bash tokenizer with custom highlighter support
15
18
  - **Headless API** -- parse markdown and extract text without any UI
16
19
  - **Streaming** -- incremental rendering for chat/LLM token streams
17
20
  - **Customizable** -- themes, per-node style overrides, custom renderers, AST transforms, plugin pipeline
@@ -21,17 +24,26 @@ Native Markdown parsing and rendering for React Native, powered by [md4c](https:
21
24
  | Dependency | Version |
22
25
  |---|---|
23
26
  | React Native | `>=0.75.0` |
24
- | react-native-nitro-modules | `>=0.35.0` |
27
+ | react-native-nitro-modules | `>=0.35.4` |
25
28
  | react-native-mathjax-svg *(optional)* | `>=0.9.0` |
26
29
  | react-native-svg *(optional, for math)* | `>=13.0.0` |
27
30
 
28
31
  ## Installation
29
32
 
33
+ With npm:
34
+
30
35
  ```bash
31
36
  npm install react-native-nitro-markdown react-native-nitro-modules
32
37
  cd ios && pod install
33
38
  ```
34
39
 
40
+ With Bun:
41
+
42
+ ```bash
43
+ bun add react-native-nitro-markdown react-native-nitro-modules
44
+ cd ios && pod install
45
+ ```
46
+
35
47
  For math rendering:
36
48
 
37
49
  ```bash
@@ -61,6 +73,22 @@ export function Example() {
61
73
 
62
74
  ## API Reference
63
75
 
76
+ ### Parser Options
77
+
78
+ `ParserOptions` controls native parser extensions. Defaults are conservative for HTML and feature-complete for Markdown extensions.
79
+
80
+ | Option | Default | Enables | Notes |
81
+ |---|---:|---|---|
82
+ | `gfm` | `true` | Tables, strikethrough, task lists, permissive autolinks | Set `false` for stricter CommonMark-style parsing |
83
+ | `math` | `true` | Inline and display LaTeX spans | Rendering falls back to plain styled text unless optional math deps are installed |
84
+ | `html` | `false` | `html_inline` and `html_block` AST nodes | Raw HTML is not rendered by default; provide custom renderers |
85
+
86
+ ```tsx
87
+ <Markdown options={{ gfm: true, math: true, html: false }}>
88
+ {content}
89
+ </Markdown>
90
+ ```
91
+
64
92
  ### `<Markdown>`
65
93
 
66
94
  The main component. Parses a markdown string and renders it.
@@ -72,9 +100,10 @@ import { Markdown } from "react-native-nitro-markdown";
72
100
  | Prop | Type | Default | Description |
73
101
  |---|---|---|---|
74
102
  | `children` | `string` | required | Markdown input string |
75
- | `options` | `ParserOptions` | -- | Parser flags (`gfm`, `math`) |
103
+ | `options` | `ParserOptions` | -- | Parser flags (`gfm`, `math`, `html`) |
76
104
  | `plugins` | `MarkdownPlugin[]` | -- | Plugin hooks (`beforeParse`, `afterParse`) |
77
105
  | `sourceAst` | `MarkdownNode` | -- | Pre-parsed AST; skips native parse when provided |
106
+ | `parseCache` | `boolean` | `true` | Enable internal parse result caching for repeated inputs |
78
107
  | `astTransform` | `AstTransform` | -- | Post-parse AST rewrite before render |
79
108
  | `renderers` | `CustomRenderers` | `{}` | Per-node custom renderer overrides |
80
109
  | `theme` | `PartialMarkdownTheme` | `defaultMarkdownTheme` | Theme token overrides |
@@ -93,6 +122,10 @@ import { Markdown } from "react-native-nitro-markdown";
93
122
 
94
123
  **Pipeline order:** `beforeParse` plugins (by priority desc) -> parse/sourceAst -> `afterParse` plugins (by priority desc) -> `astTransform` -> render.
95
124
 
125
+ When `sourceAst` is provided, `beforeParse` plugins are skipped because no source string is parsed. `afterParse` plugins still run on the provided AST.
126
+
127
+ `parseCache` defaults to `true` and caches parse results for repeated markdown inputs. Set `parseCache={false}` to force a fresh native parse on each input change. This flag has no effect when `sourceAst` is provided.
128
+
96
129
  ### `<MarkdownStream>`
97
130
 
98
131
  Renders markdown from a streaming session. Extends `MarkdownProps` (minus `children`).
@@ -174,7 +207,7 @@ import {
174
207
 
175
208
  | Function | Description |
176
209
  |---|---|
177
- | `parseMarkdown(text, options?)` | Parse to AST (options: `{ gfm?, math? }`) |
210
+ | `parseMarkdown(text, options?)` | Parse to AST (options: `{ gfm?, math?, html? }`) |
178
211
  | `parseMarkdownWithOptions(text, options)` | Parse with explicit options |
179
212
  | `extractPlainText(text)` | Parse and return plain text from native parser |
180
213
  | `extractPlainTextWithOptions(text, options)` | Same with parser flags |
@@ -218,9 +251,41 @@ Renderers receive `EnhancedRendererProps` with `node`, `children`, and `Renderer
218
251
  | `code_inline` | `content` |
219
252
  | `list` | `ordered`, `start` |
220
253
  | `task_list_item` | `checked` |
254
+ | `html_inline`, `html_block` | Read raw HTML from `node.content` |
221
255
 
222
256
  Return `undefined` to fall back to the built-in renderer, or `null` to render nothing.
223
257
 
258
+ #### Rendering HTML Nodes
259
+
260
+ HTML parsing is opt-in and produces raw AST nodes. The built-in renderer intentionally renders nothing for `html_inline` and `html_block`; applications decide what is safe to display.
261
+
262
+ ```tsx
263
+ import { Text, View } from "react-native";
264
+ import { Markdown } from "react-native-nitro-markdown";
265
+
266
+ <Markdown
267
+ options={{ html: true }}
268
+ renderers={{
269
+ html_inline: ({ node }) => {
270
+ if (node.content === "<br>") return <Text>{"\n"}</Text>;
271
+ return null;
272
+ },
273
+ html_block: ({ node }) => {
274
+ if (!node.content?.includes('data-kind="release-note"')) return null;
275
+ return (
276
+ <View>
277
+ <Text>Release note</Text>
278
+ </View>
279
+ );
280
+ },
281
+ }}
282
+ >
283
+ {content}
284
+ </Markdown>;
285
+ ```
286
+
287
+ Do not pass untrusted HTML directly into a WebView from a renderer. Sanitize or map known tags/attributes to native components.
288
+
224
289
  ### Theme API
225
290
 
226
291
  ```tsx
@@ -337,11 +402,17 @@ const plugins: MarkdownPlugin[] = [
337
402
  ```tsx
338
403
  import { Markdown, parseMarkdownWithOptions } from "react-native-nitro-markdown";
339
404
 
340
- const ast = parseMarkdownWithOptions(content, { gfm: true, math: true });
405
+ const ast = parseMarkdownWithOptions(content, {
406
+ gfm: true,
407
+ math: true,
408
+ html: false,
409
+ });
341
410
 
342
411
  <Markdown sourceAst={ast}>{"ignored when sourceAst is provided"}</Markdown>;
343
412
  ```
344
413
 
414
+ With `sourceAst`, `beforeParse` plugins are skipped, while `afterParse` and `astTransform` still apply.
415
+
345
416
  ### Virtualization (large documents)
346
417
 
347
418
  ```tsx
@@ -363,6 +434,8 @@ Keep `Markdown` as the primary vertical scroller when virtualization is enabled
363
434
  ### Syntax Highlighting
364
435
 
365
436
  ```tsx
437
+ import type { CodeHighlighter } from "react-native-nitro-markdown";
438
+
366
439
  // Built-in highlighter (JS/TS, Python, Bash)
367
440
  <Markdown highlightCode>{"```typescript\nconst x: number = 42;\n```"}</Markdown>
368
441
 
@@ -395,7 +468,7 @@ Default link behavior: trims href, calls `onLinkPress`, validates scheme (`http:
395
468
 
396
469
  `document`, `heading`, `paragraph`, `text`, `bold`, `italic`, `strikethrough`, `link`, `image`, `code_inline`, `code_block`, `blockquote`, `horizontal_rule`, `line_break`, `soft_break`, `table`, `table_head`, `table_body`, `table_row`, `table_cell`, `list`, `list_item`, `task_list_item`, `math_inline`, `math_block`, `html_block`, `html_inline`
397
470
 
398
- `html_inline` and `html_block` are parsed but not rendered by default.
471
+ `html_inline` and `html_block` are parsed only when `options.html` is `true`; they are not rendered by default. Use a custom renderer to handle them, and read raw HTML from `node.content`.
399
472
 
400
473
  ## Package Exports
401
474
 
@@ -409,10 +482,11 @@ Parser and text utilities only -- no React dependencies. Use this for server-sid
409
482
 
410
483
  ## Performance Tips
411
484
 
412
- - Use `updateStrategy="raf"` for streaming
413
- - Batch `session.append()` calls (50-100ms intervals) rather than per-character
414
- - Enable `virtualize` for large documents
415
- - Use the headless API when you don't need built-in renderers
485
+ - Use `updateStrategy="raf"` for streaming UI updates.
486
+ - Batch `session.append()` calls at 50-100ms intervals rather than per-character.
487
+ - Enable `virtualize="auto"` for long documents and keep `Markdown` as the primary vertical scroller.
488
+ - Memoize custom `plugins`, `renderers`, `theme`, and `styles` objects when they are created inside a component.
489
+ - Use the headless API when you only need parsing, plain text extraction, search indexing, or a custom renderer.
416
490
 
417
491
  ## Troubleshooting
418
492
 
@@ -432,9 +506,14 @@ The `apps/example` directory contains a full demo app with these screens:
432
506
  | Bench | `app/index.tsx` | Smoke tests + benchmark vs JS parsers |
433
507
  | Default | `app/render-default.tsx` | Built-in renderer defaults |
434
508
  | Styles | `app/render-default-styles.tsx` | Theme and style overrides |
435
- | Custom | `app/render-custom.tsx` | Custom renderers + AST transform |
509
+ | Custom | `app/render-custom.tsx` | HTML AST rendering, custom renderers, AST transform |
436
510
  | Stream | `app/render-stream.tsx` | Live streaming with token append |
437
511
 
512
+ ## Release Checks
513
+
514
+ - `bun run harness` runs lint, typecheck, JS coverage, benchmark checks, and C++ coverage.
515
+ - `bun run publish-package -- --dry-run --yes` validates release docs, runs publish verification, builds the package, and performs an npm dry run.
516
+
438
517
  ## Contributing
439
518
 
440
519
  See [CONTRIBUTING.md](./CONTRIBUTING.md).
@@ -13,6 +13,7 @@ set(CPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../cpp")
13
13
  file(GLOB MD4C_SOURCES "${CPP_ROOT}/md4c/*.c")
14
14
  file(GLOB CORE_SOURCES "${CPP_ROOT}/core/*.cpp")
15
15
  file(GLOB BINDING_SOURCES "${CPP_ROOT}/bindings/*.cpp")
16
+ list(FILTER CORE_SOURCES EXCLUDE REGEX ".*Test\\.cpp$")
16
17
 
17
18
  # Create the shared library with our sources
18
19
  add_library(${PROJECT_NAME} SHARED
@@ -44,4 +45,3 @@ target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE
44
45
  $<$<CONFIG:Release>:-O3 -DNDEBUG>
45
46
  $<$<CONFIG:Debug>:-O0 -g>
46
47
  )
47
-
@@ -62,7 +62,7 @@ class HybridMarkdownSession : HybridMarkdownSessionSpec() {
62
62
  }
63
63
 
64
64
  override fun getTextRange(from: Double, to: Double): String {
65
- if (from.isNaN() || to.isNaN() || from < 0.0) return ""
65
+ if (!from.isFinite() || !to.isFinite() || from < 0.0 || to < 0.0 || from > to) return ""
66
66
  synchronized(lock) {
67
67
  val start = from.toLong().coerceIn(0L, buffer.length.toLong()).toInt()
68
68
  val end = to.toLong().coerceIn(start.toLong(), buffer.length.toLong()).toInt()
@@ -88,15 +88,21 @@ class HybridMarkdownSession : HybridMarkdownSessionSpec() {
88
88
  }
89
89
 
90
90
  override fun replace(from: Double, to: Double, text: String): Double {
91
- require(from >= 0.0 && to >= from) { "Invalid range: from=$from must be >= 0 and to=$to must be >= from" }
91
+ require(from.isFinite() && to.isFinite() && from >= 0.0 && to >= 0.0 && to >= from) {
92
+ "Invalid range: from=$from and to=$to must be finite, from must be >= 0, and to must be >= from"
93
+ }
92
94
  val newLength: Double
95
+ val notifyFrom: Double
96
+ val notifyTo: Double
93
97
  synchronized(lock) {
94
98
  val start = from.toLong().coerceIn(0L, buffer.length.toLong()).toInt()
95
99
  val end = to.toLong().coerceIn(start.toLong(), buffer.length.toLong()).toInt()
96
100
  buffer.replace(start, end, text)
97
101
  newLength = buffer.length.toDouble()
102
+ notifyFrom = start.toDouble()
103
+ notifyTo = start.toDouble() + text.length.toDouble()
98
104
  }
99
- notifyListeners(from, from + text.length.toDouble())
105
+ notifyListeners(notifyFrom, notifyTo)
100
106
  return newLength
101
107
  }
102
108
 
@@ -30,6 +30,7 @@ target_include_directories(MD4CCore PUBLIC
30
30
  # Preprocessor definitions
31
31
  target_compile_definitions(MD4CCore PRIVATE
32
32
  MD4C_USE_UTF8=1
33
+ NITRO_MARKDOWN_TESTING=1
33
34
  )
34
35
 
35
36
  # Create the test executable
@@ -43,4 +44,4 @@ target_link_libraries(MD4CParserTest PRIVATE MD4CCore)
43
44
  # Include directories for the test
44
45
  target_include_directories(MD4CParserTest PRIVATE
45
46
  "${CPP_ROOT}/core"
46
- )
47
+ )
@@ -249,7 +249,7 @@ std::string flattenNodeText(const std::shared_ptr<InternalMarkdownNode>& node) {
249
249
  } // namespace
250
250
 
251
251
  std::string HybridMarkdownParser::parse(const std::string& text) {
252
- InternalParserOptions opts{.gfm = true, .math = true};
252
+ InternalParserOptions opts{.gfm = true, .math = true, .html = false};
253
253
 
254
254
  auto ast = parser_->parse(text, opts);
255
255
  return nodeToJson(ast);
@@ -259,13 +259,14 @@ std::string HybridMarkdownParser::parseWithOptions(const std::string& text, cons
259
259
  InternalParserOptions internalOpts;
260
260
  internalOpts.gfm = options.gfm.value_or(true);
261
261
  internalOpts.math = options.math.value_or(true);
262
+ internalOpts.html = options.html.value_or(false);
262
263
 
263
264
  auto ast = parser_->parse(text, internalOpts);
264
265
  return nodeToJson(ast);
265
266
  }
266
267
 
267
268
  std::string HybridMarkdownParser::extractPlainText(const std::string& text) {
268
- InternalParserOptions opts{.gfm = true, .math = true};
269
+ InternalParserOptions opts{.gfm = true, .math = true, .html = false};
269
270
 
270
271
  auto ast = parser_->parse(text, opts);
271
272
  return flattenNodeText(ast);
@@ -275,6 +276,7 @@ std::string HybridMarkdownParser::extractPlainTextWithOptions(const std::string&
275
276
  InternalParserOptions internalOpts;
276
277
  internalOpts.gfm = options.gfm.value_or(true);
277
278
  internalOpts.math = options.math.value_or(true);
279
+ internalOpts.html = options.html.value_or(false);
278
280
 
279
281
  auto ast = parser_->parse(text, internalOpts);
280
282
  return flattenNodeText(ast);
@@ -424,10 +424,28 @@ public:
424
424
 
425
425
  case MD_TEXT_HTML:
426
426
  impl->flushText();
427
- if (!impl->nodeStack.empty()) {
427
+ if (!impl->nodeStack.empty() && text && size > 0) {
428
+ MD_OFFSET off = safeOffset(text, impl->inputText, impl->inputTextSize);
429
+ if (off == 0 && text != impl->inputText) off = impl->lastTextEnd;
430
+
431
+ if (impl->nodeStack.top()->type == NodeType::HtmlBlock) {
432
+ auto htmlBlock = impl->nodeStack.top();
433
+ if (htmlBlock->content.has_value()) {
434
+ htmlBlock->content->append(text, size);
435
+ } else {
436
+ htmlBlock->content = std::string(text, size);
437
+ }
438
+ htmlBlock->end = off + size;
439
+ impl->lastTextEnd = off + size;
440
+ break;
441
+ }
442
+
428
443
  auto node = std::make_shared<MarkdownNode>(NodeType::HtmlInline);
429
444
  node->content = std::string(text, size);
445
+ node->beg = off;
446
+ node->end = off + size;
430
447
  impl->nodeStack.top()->addChild(node);
448
+ impl->lastTextEnd = off + size;
431
449
  }
432
450
  break;
433
451
 
@@ -471,12 +489,20 @@ MD4CParser::MD4CParser() : impl_(std::make_unique<Impl>()) {}
471
489
  MD4CParser::~MD4CParser() = default;
472
490
 
473
491
  std::shared_ptr<MarkdownNode> MD4CParser::parse(const std::string& markdown, const ParserOptions& options) {
492
+ return parseWithFlags(markdown, options, 0);
493
+ }
494
+
495
+ std::shared_ptr<MarkdownNode> MD4CParser::parseWithFlags(
496
+ const std::string& markdown,
497
+ const ParserOptions& options,
498
+ unsigned int extraFlags
499
+ ) {
474
500
  impl_->reset();
475
501
  impl_->inputText = markdown.c_str();
476
502
  size_t inputSize = clampInputSize(markdown.size());
477
503
  impl_->inputTextSize = inputSize;
478
504
 
479
- unsigned int flags = MD_FLAG_NOHTML;
505
+ unsigned int flags = options.html ? 0 : MD_FLAG_NOHTML;
480
506
 
481
507
  if (options.gfm) {
482
508
  flags |= MD_FLAG_TABLES;
@@ -488,6 +514,7 @@ std::shared_ptr<MarkdownNode> MD4CParser::parse(const std::string& markdown, con
488
514
  if (options.math) {
489
515
  flags |= MD_FLAG_LATEXMATHSPANS;
490
516
  }
517
+ flags |= extraFlags;
491
518
 
492
519
  MD_PARSER parser = {
493
520
  0,
@@ -515,4 +542,44 @@ std::shared_ptr<MarkdownNode> MD4CParser::parse(const std::string& markdown, con
515
542
  return impl_->root;
516
543
  }
517
544
 
545
+ #ifdef NITRO_MARKDOWN_TESTING
546
+ std::shared_ptr<MarkdownNode> MD4CParser::parseWithExtraFlagsForTest(
547
+ const std::string& markdown,
548
+ const ParserOptions& options,
549
+ unsigned int extraFlags
550
+ ) {
551
+ return parseWithFlags(markdown, options, extraFlags);
552
+ }
553
+
554
+ int MD4CParser::enterBlockNullUserdataForTest() {
555
+ return Impl::enterBlock(MD_BLOCK_DOC, nullptr, 0, nullptr);
556
+ }
557
+
558
+ int MD4CParser::leaveBlockNullUserdataForTest() {
559
+ return Impl::leaveBlock(MD_BLOCK_DOC, nullptr, 0, nullptr);
560
+ }
561
+
562
+ int MD4CParser::enterSpanNullUserdataForTest() {
563
+ return Impl::enterSpan(MD_SPAN_EM, nullptr, 0, nullptr);
564
+ }
565
+
566
+ int MD4CParser::leaveSpanNullUserdataForTest() {
567
+ return Impl::leaveSpan(MD_SPAN_EM, nullptr, 0, nullptr);
568
+ }
569
+
570
+ int MD4CParser::textNullUserdataForTest() {
571
+ return Impl::text(MD_TEXT_NORMAL, "x", 1, nullptr);
572
+ }
573
+
574
+ int MD4CParser::offsetBeforeBaseForTest() {
575
+ char buffer[2] = {'a', 'b'};
576
+ return safeOffset(buffer, buffer + 1, 1);
577
+ }
578
+
579
+ int MD4CParser::offsetPastBaseForTest() {
580
+ char buffer[2] = {'a', 'b'};
581
+ return safeOffset(buffer + 1, buffer, 0);
582
+ }
583
+ #endif
584
+
518
585
  } // namespace NitroMarkdown
@@ -19,6 +19,18 @@ public:
19
19
  std::shared_ptr<MarkdownNode> parse(const std::string& markdown, const ParserOptions& options);
20
20
 
21
21
  #ifdef NITRO_MARKDOWN_TESTING
22
+ std::shared_ptr<MarkdownNode> parseWithExtraFlagsForTest(
23
+ const std::string& markdown,
24
+ const ParserOptions& options,
25
+ unsigned int extraFlags
26
+ );
27
+ static int enterBlockNullUserdataForTest();
28
+ static int leaveBlockNullUserdataForTest();
29
+ static int enterSpanNullUserdataForTest();
30
+ static int leaveSpanNullUserdataForTest();
31
+ static int textNullUserdataForTest();
32
+ static int offsetBeforeBaseForTest();
33
+ static int offsetPastBaseForTest();
22
34
  static size_t clampInputSizeForTest(size_t inputSize) {
23
35
  size_t maxSize = static_cast<size_t>(std::numeric_limits<MD_SIZE>::max());
24
36
  return inputSize > maxSize ? maxSize : inputSize;
@@ -27,6 +39,11 @@ public:
27
39
 
28
40
  private:
29
41
  class Impl;
42
+ std::shared_ptr<MarkdownNode> parseWithFlags(
43
+ const std::string& markdown,
44
+ const ParserOptions& options,
45
+ unsigned int extraFlags
46
+ );
30
47
  std::unique_ptr<Impl> impl_;
31
48
  };
32
49
 
@@ -118,7 +118,7 @@ struct MarkdownNode {
118
118
  struct ParserOptions {
119
119
  bool gfm = true;
120
120
  bool math = true;
121
+ bool html = false;
121
122
  };
122
123
 
123
124
  } // namespace NitroMarkdown
124
-
@@ -47,7 +47,7 @@ try {
47
47
  /**
48
48
  * Parse markdown text with custom options.
49
49
  * @param text - The markdown text to parse
50
- * @param options - Parser options (gfm, math)
50
+ * @param options - Parser options (gfm, math, html)
51
51
  * @returns The root node of the parsed AST
52
52
  */
53
53
 
@@ -72,7 +72,7 @@ function parseMarkdown(text, options) {
72
72
  /**
73
73
  * Parse markdown text with custom options.
74
74
  * @param text - The markdown text to parse
75
- * @param options - Parser options (gfm, math)
75
+ * @param options - Parser options (gfm, math, html)
76
76
  * @returns The root node of the parsed AST
77
77
  */
78
78
  function parseMarkdownWithOptions(text, options) {