red_quilt 0.7.1 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97ab1d8ff3dcb3278403b6f85fba5c49bfef8fa9fa2aceac979873fd580260ba
4
- data.tar.gz: b604fecab6bf8f3e3ab06a768cc3625e7adbcca3f256334efcf7c9c2e1c4fcd8
3
+ metadata.gz: 56e09a19cfb78fad2a7e9a377f3ec6b9033d2160d4c126719e8220245b340709
4
+ data.tar.gz: 35bcb8de686345a72b9d2edf0261c7b201fb138047b571e3cc14b4f62be2671a
5
5
  SHA512:
6
- metadata.gz: 2709412545b3b9c28752f6da004781bcf628874db68eea0753231951f3c73a89a5bdadf9250e7940db77f1e8d4d5dca89bd4ed04c428c4f764fcdb7d278baf85
7
- data.tar.gz: 27568989536531184a37814fd84b81961887ddfc4de9de3cc05de6135930833a06578cb36c8ffff3f9f95abd6dd7c4ad6894281b7f72a2f8e33ef65fb583e44c
6
+ metadata.gz: a5ad64e49f61ddcca8c3453b28ab06e7c04d76da574d82e6185b9a175189f313e043dcb586ff4293ad801b6bbb521ecca902f4dd9cac630186db97a2d8de5b91
7
+ data.tar.gz: fc3764a74f06f1d902afd5eeba1e572ddb97f3fa1c9306f4de937e1929937d22830f856c77b03d2e1fc658be3310d7f7535cb994a7945cf7f5f18ea52e0416c4
data/CHANGELOG.md CHANGED
@@ -5,6 +5,48 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - `NodeRef#info`: returns the fence info string of a code block (e.g. `ruby`
13
+ in ` ```ruby `, or `vtt audio="x.mp3"`); `""` for code blocks without one
14
+ and for every other node type. The raw code content remains available via
15
+ `NodeRef#text`.
16
+ - `Renderer::HTML#render_fragment(nodes)`: renders an Array of `NodeRef` in
17
+ order and returns the HTML fragment without affecting the main render
18
+ output. Renderer state shared across nodes (e.g. the heading-id slugger) is
19
+ preserved between calls. Lets callers that partition a document render the
20
+ pieces separately without reaching into renderer internals.
21
+ - `Arena` semantic payload accessors (`heading_level`, `list_ordered?`,
22
+ `code_block_info`, `link_destination`, `footnote_number`, …) for callers
23
+ that walk `Document#arena`, replacing direct use of the raw `int1`/`str2`
24
+ columns.
25
+
26
+ ## [0.7.2] - 2026-06-23
27
+
28
+ ### Added
29
+
30
+ - Opt-in YAML frontmatter support via the `frontmatter:` option on `parse` /
31
+ `render_html` and the `--frontmatter` CLI flag (off by default). A leading
32
+ `---` … `---` block is removed from the rendered body and exposed as
33
+ `Document#frontmatter`; in standalone output its `title` / `lang` keys fill
34
+ in `<title>` / `<html lang>`.
35
+ - Opt-in Mermaid diagram support via the `mermaid:` option on `render_html` /
36
+ `Document#to_html` and the `--mermaid` CLI flag (off by default). Fenced
37
+ ` ```mermaid ` code blocks render as `<pre class="mermaid">` containers; in
38
+ standalone output the mermaid.js runtime is loaded from a CDN and each
39
+ diagram is made interactive (wheel zoom, drag pan, +/-/reset controls) with
40
+ svg-pan-zoom.
41
+
42
+ ## [0.7.1] - 2026-06-06
43
+
44
+ ### Added
45
+
46
+ - `--open` CLI flag: render the Markdown to a standalone HTML file and open it
47
+ in the default browser (forces `--standalone`; writes under `Dir.tmpdir`
48
+ when `-o` is not given).
49
+
8
50
  ## [0.7.0] - 2026-05-29
9
51
 
10
52
  ### Added
data/README.md CHANGED
@@ -48,6 +48,7 @@ RedQuilt.render_html("Hi <em>tag</em>", allow_html: true)
48
48
  | `extended_autolinks:` | `false` | GFM: linkify bare `http(s)://` / `www.` / email addresses |
49
49
  | `footnotes:` | `false` | GFM footnotes (see below) |
50
50
  | `lint:` | `false` | Collect lint diagnostics (empty links, missing image alt, heading-level skips) |
51
+ | `frontmatter:` | `false` | Parse leading YAML frontmatter (`---`) as metadata (see below) |
51
52
 
52
53
  ### Footnotes (opt-in)
53
54
 
@@ -75,14 +76,66 @@ doc.diagnostics.first.severity # => :warning
75
76
  ### Heading anchors (opt-in)
76
77
 
77
78
  `render_html` / `to_html` accept `heading_ids:` to give every heading a
78
- slugified `id` for anchor links. Slugs follow GitHub's scheme but keep Unicode
79
- intact, so Japanese headings stay readable; duplicates get `-1`, `-2` suffixes.
79
+ slugified `id` for anchor links.
80
80
 
81
81
  ```ruby
82
82
  RedQuilt.render_html("# Hello World\n\n## はじめに", heading_ids: true)
83
83
  # => "<h1 id=\"hello-world\">Hello World</h1>\n<h2 id=\"はじめに\">はじめに</h2>\n"
84
84
  ```
85
85
 
86
+ ### Mermaid diagrams (opt-in)
87
+
88
+ `render_html` / `to_html` accept `mermaid:` to render ` ```mermaid ` fenced
89
+ code blocks for [mermaid.js](https://mermaid.js.org/).
90
+ In standalone output the mermaid.js runtime is also loaded from a CDN.
91
+
92
+ ```ruby
93
+ RedQuilt.render_html("```mermaid\ngraph LR\n A --> B\n```", mermaid: true)
94
+ # => "<pre class=\"mermaid\">graph LR\n A --&gt; B\n</pre>\n"
95
+
96
+ # Full page that renders the diagram in a browser (CDN script included):
97
+ RedQuilt.parse("```mermaid\ngraph LR\n A --> B\n```")
98
+ .to_html(standalone: true, mermaid: true)
99
+ ```
100
+
101
+ In standalone output each diagram is made interactive with
102
+ [svg-pan-zoom](https://github.com/bumbu/svg-pan-zoom) (loaded from a CDN).
103
+
104
+ ### YAML frontmatter (opt-in)
105
+
106
+ `parse` / `render_html` accept `frontmatter:` to extract a leading YAML
107
+ frontmatter block (the `---` … `---` fences used by Jekyll, Hugo).
108
+ The block is parsed with `Psych.safe_load` and removed from the rendered body;
109
+ the parsed Hash is exposed as `Document#frontmatter`.
110
+
111
+ ```ruby
112
+ doc = RedQuilt.parse(<<~MD, frontmatter: true)
113
+ ---
114
+ title: My Page
115
+ lang: ja
116
+ ---
117
+
118
+ # Body
119
+ MD
120
+ doc.frontmatter # => {"title" => "My Page", "lang" => "ja"}
121
+ doc.to_html # => "<h1>Body</h1>\n" (frontmatter stripped)
122
+ ```
123
+
124
+ In standalone output the frontmatter's `title` / `lang` fill in `<title>` /
125
+ `<html lang>` when no explicit argument is given (explicit argument >
126
+ frontmatter > default):
127
+
128
+ ```ruby
129
+ doc.to_html(standalone: true)
130
+ # <html lang="ja"> … <title>My Page</title> …
131
+ ```
132
+
133
+ The feature is opt-in, so a bare `---` is never mistaken for frontmatter
134
+ unless `frontmatter: true` is passed. Frontmatter lines are blanked rather
135
+ than deleted, so body source spans and diagnostic line numbers stay relative
136
+ to the start of the file. Invalid YAML records a `:frontmatter` warning
137
+ diagnostic and leaves `Document#frontmatter` as `nil` without raising.
138
+
86
139
  ### Tilt integration
87
140
 
88
141
  RedQuilt ships a [Tilt](https://github.com/jeremyevans/tilt) adapter.
@@ -100,13 +153,13 @@ Native options (`allow_html:`, `footnotes:`, …) pass straight through; Tilt's
100
153
  ## Documentation
101
154
 
102
155
  - [API reference](docs/api.md) — `Document` / `NodeRef` / `SourceSpan`, supported syntax, and usage examples
103
- - [Architecture overview](docs/architecture.ja.md) (日本語)
104
- - [Arena usage guide](docs/arena-usage.ja.md) (日本語)
105
- - [CommonMark conformance notes](docs/commonmark-conformance.ja.md) (日本語)
156
+ - [Architecture overview](docs/architecture.md) ([日本語](docs/architecture.ja.md))
157
+ - [Arena usage guide](docs/arena-usage.md) ([日本語](docs/arena-usage.ja.md))
158
+ - [CommonMark conformance notes](docs/commonmark-conformance.md) ([日本語](docs/commonmark-conformance.ja.md))
106
159
 
107
160
  ## CommonMark Compatibility
108
161
 
109
- RedQuilt achieves 100% compliance with the CommonMark v0.31.2 specification.
162
+ RedQuilt achieves 100% compliance with the CommonMark v0.31.2 test cases.
110
163
  See the [conformance notes](docs/commonmark-conformance.ja.md) for GFM
111
164
  extensions and intentional deviations.
112
165
 
@@ -143,6 +196,9 @@ redquilt -o output.html input.md
143
196
 
144
197
  # Render and open the result in the default browser
145
198
  redquilt --open input.md
199
+
200
+ # Render mermaid code blocks as diagrams (loads mermaid.js from a CDN)
201
+ redquilt --mermaid --open input.md
146
202
  ```
147
203
 
148
204
  ### Options
@@ -164,6 +220,10 @@ redquilt --open input.md
164
220
  --open Write HTML to a file and open it in the default
165
221
  browser (forces --standalone; uses a file under
166
222
  Dir.tmpdir when -o is not given)
223
+ --mermaid Render `mermaid` code blocks as diagrams (loads
224
+ mermaid.js from a CDN in standalone output)
225
+ --frontmatter Parse leading YAML frontmatter (---) as metadata;
226
+ fills <title>/lang in standalone output
167
227
  --diagnostics Print diagnostics to stderr
168
228
  --diagnostics-only Print diagnostics only (suppress output)
169
229
  -h, --help Show help
@@ -216,7 +276,9 @@ RedQuilt.render_html(user_markdown, allow_html: true)
216
276
  bundle exec rake spec
217
277
  ```
218
278
 
219
- Runs 70+ CommonMark compatibility and feature tests.
279
+ Runs the full CommonMark 0.31.2 conformance suite (all 652 official examples,
280
+ parsed directly from `spec/fixtures/cmark_spec-0.31.2.md`) plus RedQuilt's own
281
+ feature tests — 1000 examples in total.
220
282
 
221
283
  ### Benchmark
222
284
 
data/Rakefile CHANGED
@@ -6,3 +6,34 @@ require "rspec/core/rake_task"
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  task default: :spec
9
+
10
+ desc "Report CommonMark spec conformance, broken down by section"
11
+ task :conformance do
12
+ require_relative "lib/red_quilt"
13
+ require_relative "spec/support/commonmark_spec_loader"
14
+
15
+ examples = CommonMarkSpecLoader.examples
16
+ sections = examples.each_with_object({}) do |example, acc|
17
+ stats = acc[example[:section]] ||= { pass: 0, total: 0 }
18
+ stats[:total] += 1
19
+ actual = RedQuilt.parse(example[:markdown], allow_html: true).to_html
20
+ stats[:pass] += 1 if actual == example[:html]
21
+ end
22
+
23
+ total = { pass: sections.values.sum { |s| s[:pass] }, total: examples.size }
24
+ width = sections.keys.map(&:length).max
25
+ divider = " #{'-' * width} ---------- -------"
26
+
27
+ puts "CommonMark #{CommonMarkSpecLoader::VERSION} conformance", ""
28
+ puts format(" %-#{width}s %-10s %s", "Section", "Pass/Total", "Rate")
29
+ puts divider
30
+ sections.each do |name, stats|
31
+ rate = stats[:pass].fdiv(stats[:total]) * 100
32
+ puts format(" %-#{width}s %4d / %4d %5.1f%%", name, stats[:pass], stats[:total], rate)
33
+ end
34
+ puts divider
35
+ rate = total[:pass].fdiv(total[:total]) * 100
36
+ puts format(" %-#{width}s %4d / %4d %5.1f%%", "TOTAL", total[:pass], total[:total], rate)
37
+
38
+ abort("\nConformance is below 100%.") unless total[:pass] == total[:total]
39
+ end
data/docs/api.md CHANGED
@@ -18,11 +18,25 @@ doc.source_map # Line/column lookup (lazy memoized)
18
18
  doc.diagnostics # Array of RedQuilt::Diagnostic collected while parsing
19
19
  doc.allow_html? # Check HTML pass-through setting
20
20
  doc.disallow_raw_html? # Check GFM disallowed-raw-HTML filtering setting
21
+ doc.frontmatter # Parsed YAML frontmatter Hash, or nil (see below)
21
22
 
22
23
  # Standalone document with an embedded theme:
23
24
  doc.to_html(standalone: true, theme: :default, title: "My Doc", lang: "en")
24
25
  # theme: :default (compact, dark-mode-aware stylesheet) or :none (bare).
25
26
  # css: "style.css" links an external stylesheet instead.
27
+
28
+ # Render `mermaid` code blocks as <pre class="mermaid"> diagrams; in
29
+ # standalone mode the mermaid.js runtime is loaded from a CDN too.
30
+ doc.to_html(standalone: true, mermaid: true)
31
+
32
+ # Parse a leading YAML frontmatter block (--- ... ---). Off by default; when
33
+ # enabled the block is removed from the rendered body and exposed as a Hash.
34
+ doc = RedQuilt.parse("---\ntitle: Hi\nlang: ja\n---\n\n# Body", frontmatter: true)
35
+ doc.frontmatter # => {"title" => "Hi", "lang" => "ja"} (nil when absent/disabled)
36
+ # In standalone output frontmatter title/lang fill <title>/<html lang> unless
37
+ # an explicit argument overrides them. Invalid YAML adds a :frontmatter
38
+ # warning diagnostic and leaves doc.frontmatter as nil.
39
+ doc.to_html(standalone: true)
26
40
  ```
27
41
 
28
42
  ## NodeRef (AST node wrapper)
@@ -36,6 +50,7 @@ node.children # Array[NodeRef]
36
50
  node.walk # Enumerator[NodeRef] or { |node| ... } block
37
51
  node.find_all(:link) # Array[NodeRef] with matching type
38
52
  node.text # String (concatenated child text)
53
+ node.info # String fence info of a code block (e.g. "ruby")
39
54
 
40
55
  # Position information (byte offset)
41
56
  node.source_span # SourceSpan with start_byte, end_byte
@@ -35,7 +35,7 @@ Source (Markdown String)
35
35
  ## 各ステージの責務
36
36
 
37
37
  ### `RedQuilt.normalize_input`
38
- CommonMark§2.3/2.4の最小前処理。`\r\n`/`\r`→`\n`の行末正規化と、NUL→U+FFFDの置換だけを行う。
38
+ - CommonMark§2.3/2.4の最小前処理。`\r\n`/`\r`→`\n`の行末正規化と、NUL→U+FFFDの置換だけを行う。
39
39
 
40
40
  ### BlockParser
41
41
  - 行分割: sourceを`Line` Struct配列へ。各行はbyte spanで保持する。
@@ -0,0 +1,99 @@
1
+ # RedQuilt Architecture Overview
2
+
3
+ This document gives a high-level view of how RedQuilt is structured.
4
+
5
+ ## Pipeline
6
+
7
+ ```
8
+ Source (Markdown String)
9
+
10
+ ▼ RedQuilt.normalize_input (lib/red_quilt.rb)
11
+
12
+ ▼ BlockParser (lib/red_quilt/block_parser.rb)
13
+ │ dispatch / container parsers / build_lines
14
+ │ (list.rb, blockquote.rb, reference_definition.rb)
15
+
16
+ ▼ Arena (raw inline spans)
17
+ │ The body of each paragraph / heading / table cell is kept
18
+ │ as a byte span or a str1 literal.
19
+
20
+ ▼ InlinePass (lib/red_quilt/inline_pass.rb)
21
+ │ ├─ Inline::Lexer (lib/red_quilt/inline/lexer.rb)
22
+ │ │ byte scan -> Tokens (parallel array)
23
+ │ └─ Inline::Builder (lib/red_quilt/inline/builder.rb)
24
+ │ linear pass -> process_emphasis (CommonMark §6.2)
25
+
26
+ ▼ Arena (inline resolved)
27
+
28
+ ▼ (option) FootnotePass (footnotes: true)
29
+ ▼ (option) ExtendedAutolinkPass (extended_autolinks: true)
30
+ ▼ (option) LintPass (lint: true)
31
+
32
+ ▼ Renderer::HTML (lib/red_quilt/renderer/html.rb)
33
+ walk the arena and append to a mutable String
34
+ ```
35
+
36
+ ## Responsibility of each stage
37
+
38
+ ### `RedQuilt.normalize_input`
39
+ - Minimal preprocessing required by CommonMark §2.3 / §2.4. It only normalizes
40
+ line endings (`\r\n` / `\r` -> `\n`) and replaces NUL with U+FFFD.
41
+
42
+ ### BlockParser
43
+ - Line splitting: turn the source into an array of `Line` structs. Each line is
44
+ kept as a byte span.
45
+ - Dispatch: decide the block kind from the first byte of the line
46
+ (`paragraph_only_line?` quickly routes non-block lines).
47
+ - Container delegation: lists and blockquotes are delegated to `List::Parser`
48
+ and `Blockquote::Parser`, which call `parse_lines` recursively.
49
+ - Collecting and excluding definitions: link reference definitions (the
50
+ reference table) and opt-in footnote definitions (`FootnoteRegistry`) are
51
+ pulled out of the body flow and gathered in dedicated collectors.
52
+ - Column calculation: indentation that includes tab expansion is delegated to
53
+ `Indentation`.
54
+ - Output: build block nodes in the Arena, with inline content still unresolved.
55
+
56
+ ### InlinePass / Inline::Lexer / Inline::Builder
57
+ - Target selection: scan and process each inline target (paragraph / heading /
58
+ table cell).
59
+ - Lexer: scan the target's byte span, or the range of a str1 literal, into
60
+ Tokens (a parallel array).
61
+ - Builder, step 1 (linear_pass): resolve code spans, links, images, autolinks,
62
+ and simple inlines.
63
+ - Builder, step 2 (process_emphasis): collapse the delimiter stack to finalize
64
+ emphasis / strong / strikethrough (CommonMark §6.2; strikethrough is a GFM
65
+ extension).
66
+ - Footnote references: resolve `[^label]` through `FootnoteRegistry`, number
67
+ them in first-reference order, and create a `FOOTNOTE_REFERENCE`.
68
+
69
+ ### FootnotePass (`footnotes: true`)
70
+ - Reordering: sort the definitions under `FOOTNOTES_SECTION` (at the end of the
71
+ root) into first-reference order.
72
+ - Pruning: detach unreferenced definitions.
73
+ - Section removal: if there are no references at all, remove the section itself.
74
+
75
+ ### Renderer::HTML
76
+ - Walk: walk the arena recursively and append directly with `<<` to a mutable
77
+ String opened with `+""`.
78
+ - Raw HTML: `allow_html` switches between passing HTML through and escaping it;
79
+ `disallow_raw_html` filters HTML using GFM "Disallowed Raw HTML".
80
+ - Footnotes: render `FOOTNOTE_REFERENCE` as a sup link, and the trailing
81
+ `FOOTNOTES_SECTION` as `<section class="footnotes">` with backrefs.
82
+
83
+ ## Where the main subsystems live
84
+
85
+ | Area | Files |
86
+ |---|---|
87
+ | Entry point / input normalization | `lib/red_quilt.rb` |
88
+ | Public API | `lib/red_quilt/document.rb`, `node_ref.rb` |
89
+ | Arena | `lib/red_quilt/arena.rb` |
90
+ | Block parsing | `block_parser.rb`, `list.rb`, `blockquote.rb`, `indentation.rb` |
91
+ | Reference definitions | `reference_definition.rb` |
92
+ | Footnotes (opt-in) | `footnote_definition.rb`, `footnote_registry.rb`, `footnote_pass.rb` |
93
+ | Inline parsing | `inline.rb`, `inline/lexer.rb`, `inline/tokens.rb`, `inline/flanking.rb`, `inline/builder.rb`, `inline/link_scanner.rb` |
94
+ | Inline entities | `inline/html_entities.rb` |
95
+ | HTML / MDAST output | `renderer/html.rb`, `renderer/mdast.rb` |
96
+ | Extension passes | `inline_pass.rb`, `footnote_pass.rb`, `extended_autolink_pass.rb`, `lint_pass.rb` |
97
+ | Source positions | `source_span.rb`, `source_map.rb` |
98
+ | Diagnostics | `diagnostic.rb` |
99
+ | CLI | `cli.rb`, `exe/redquilt` |
@@ -92,7 +92,7 @@ ArenaはASTを「オブジェクトのツリー」ではなく[parallel array](h
92
92
  - メモリ局所性が良く、GC圧が小さい
93
93
  - ノードを「軽い」値として扱えるのでRenderer / Builderをinline化しやすい
94
94
 
95
- ###列(column)一覧
95
+ #### 列(column)一覧
96
96
 
97
97
  |列名|用途|
98
98
  |------|------|
@@ -183,7 +183,7 @@ Arenaの公開メソッドは以下の3レイヤーに分けて読むと意図
183
183
 
184
184
  各NodeTypeがどのint / strスロットを使うかは規約で決まっています。以下が現在の規約です。
185
185
 
186
- ### Blockノード
186
+ #### Blockノード
187
187
 
188
188
  | NodeType | int1 | int2 | int3 | str1 | str2 |
189
189
  |----------|------|------|------|------|------|
@@ -202,7 +202,7 @@ Arenaの公開メソッドは以下の3レイヤーに分けて読むと意図
202
202
  | `FOOTNOTE_DEFINITION` | - | - | - | 正規化済みlabel | - |
203
203
  | `FOOTNOTES_SECTION` | - | - | - | - | - |
204
204
 
205
- ### Inlineノード
205
+ #### Inlineノード
206
206
 
207
207
  | NodeType | int1 | int2 | int3 | str1 | str2 |
208
208
  |----------|------|------|------|------|------|
@@ -219,7 +219,7 @@ Arenaの公開メソッドは以下の3レイヤーに分けて読むと意図
219
219
 
220
220
  > footnoteは`footnotes: true`時のみ生成されます。`FOOTNOTES_SECTION`はroot直下の最後の子として置かれ(span-less、`source_start: -1`)、参照された`FOOTNOTE_DEFINITION`を初回参照順に保持します。backrefの個数はfootnote番号とlabelからrender時に算出します。
221
221
 
222
- ### Source spanの慣習
222
+ #### Source spanの慣習
223
223
 
224
224
  - `source_start` / `source_len`: 元documentのbytes (絶対byte offset)
225
225
  - `source_start < 0`: spanなし。leafノードでは内容を`str1`にliteralとして持つことが多いが、container inlineは子ノードだけを持つ場合がある。
@@ -334,7 +334,7 @@ arena.update_span(text_id, 0, 12)
334
334
 
335
335
  ## 6. パフォーマンス上の注意
336
336
 
337
- ####ホットパスでは`each_child`を使う
337
+ #### ホットパスでは`each_child`を使う
338
338
 
339
339
  ブロック直yieldでEnumerator allocationを避ける。`child_ids`は外部API用
340
340