red_quilt 0.7.0 → 0.7.2
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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +41 -7
- data/Rakefile +31 -0
- data/docs/api.md +4 -0
- data/docs/architecture.ja.md +4 -4
- data/docs/architecture.md +99 -0
- data/docs/arena-usage.ja.md +5 -5
- data/docs/arena-usage.md +423 -0
- data/docs/commonmark-conformance.md +316 -0
- data/lib/red_quilt/browser_launcher.rb +37 -0
- data/lib/red_quilt/cli.rb +54 -9
- data/lib/red_quilt/document.rb +70 -4
- data/lib/red_quilt/renderer/html.rb +16 -6
- data/lib/red_quilt/version.rb +1 -1
- data/lib/red_quilt.rb +2 -2
- metadata +7 -3
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# RedQuilt CommonMark Conformance
|
|
2
|
+
|
|
3
|
+
## 1. Scope of this document
|
|
4
|
+
|
|
5
|
+
This document describes how RedQuilt differs from the CommonMark / GFM spec.
|
|
6
|
+
For behavior that follows the spec, refer directly to the spec documents
|
|
7
|
+
(<https://spec.commonmark.org/0.31.2/>, <https://github.github.com/gfm/>); this
|
|
8
|
+
document does not repeat them.
|
|
9
|
+
|
|
10
|
+
#### What this document covers
|
|
11
|
+
|
|
12
|
+
- Places where the implementation **narrows** what the spec allows (interpreting
|
|
13
|
+
or tightening ambiguous areas).
|
|
14
|
+
- Features outside the spec (security, diagnostics, option flags).
|
|
15
|
+
- The extensions that are **enabled** (GFM, etc.) and their opt-in conditions.
|
|
16
|
+
- Unsupported features and known limitations.
|
|
17
|
+
|
|
18
|
+
#### What this document does not cover
|
|
19
|
+
|
|
20
|
+
- Descriptions of standard behavior that matches the spec.
|
|
21
|
+
- Design background or data structure choices.
|
|
22
|
+
|
|
23
|
+
### 1.1 Target versions
|
|
24
|
+
|
|
25
|
+
- CommonMark: 0.31.2
|
|
26
|
+
- GitHub Flavored Markdown: 0.29-gfm
|
|
27
|
+
|
|
28
|
+
### 1.2 Implementation assumptions
|
|
29
|
+
|
|
30
|
+
- Input is a UTF-8 string. Preprocessing such as `force_encoding(Encoding::UTF_8)`
|
|
31
|
+
is the caller's responsibility.
|
|
32
|
+
- The normalization required by spec §2.3 / §2.4 (NUL -> U+FFFD,
|
|
33
|
+
`\r\n` / `\r` -> `\n`) and limiting the blank-line definition to space/tab are
|
|
34
|
+
all implemented. These follow the spec, so this document does not list them
|
|
35
|
+
individually.
|
|
36
|
+
|
|
37
|
+
### 1.3 Format of each item
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
### N.N <Title>
|
|
41
|
+
|
|
42
|
+
**Spec**: the relevant section and the spec rule (or ambiguity)
|
|
43
|
+
**RedQuilt behavior**: how the implementation behaves / where it narrows or extends
|
|
44
|
+
**Implementation**: file:line / main symbols
|
|
45
|
+
**Test**: spec file / example number
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 2. Points where the spec is tightened
|
|
49
|
+
|
|
50
|
+
Where the spec wording allows more than one interpretation, or where a "must" is
|
|
51
|
+
left ambiguous, the implementation chooses the stricter side.
|
|
52
|
+
|
|
53
|
+
### 2.1 URI autolink rejects U+007F (DEL)
|
|
54
|
+
|
|
55
|
+
**Spec**: §6.5 — a URI autolink does not contain "ASCII control characters,
|
|
56
|
+
space, `<`, `>`". Whether the range of "ASCII control characters" is only
|
|
57
|
+
U+0000–U+001F or also includes U+007F is not stated.
|
|
58
|
+
|
|
59
|
+
**RedQuilt behavior**: also rejects U+007F.
|
|
60
|
+
|
|
61
|
+
**Implementation**: `lib/red_quilt/inline/lexer.rb` — `URI_AUTOLINK_RE`
|
|
62
|
+
|
|
63
|
+
**Test**: `spec/whitespace_strictness_spec.rb` — "URI autolink (CommonMark 6.5)"
|
|
64
|
+
|
|
65
|
+
### 2.2 Raw HTML tag separators limited to space/tab/CR/LF
|
|
66
|
+
|
|
67
|
+
**Spec**: §6.6 — defines the separators between attributes and around `=` as
|
|
68
|
+
"whitespace". In the spec's terminology (§2.1), the "whitespace" set is broad and
|
|
69
|
+
includes space / tab / newline / line tabulation (U+000B) / form feed (U+000C) /
|
|
70
|
+
carriage return.
|
|
71
|
+
|
|
72
|
+
**RedQuilt behavior**: within the tag grammar, only `[ \t\r\n]` is allowed as a
|
|
73
|
+
separator. FF (U+000C) / VT (U+000B) are not included. The same constraint
|
|
74
|
+
applies to inline raw HTML and to HTML block types 1 / 6 / 7.
|
|
75
|
+
|
|
76
|
+
**Implementation**:
|
|
77
|
+
- Inline: `lib/red_quilt/inline/lexer.rb` — `HTML_OPEN_TAG_RE` /
|
|
78
|
+
`HTML_CLOSING_TAG_RE`
|
|
79
|
+
- Block: `lib/red_quilt/block_parser.rb` — `HTML_TYPE_7_OPEN_TAG_RE` /
|
|
80
|
+
`HTML_TYPE_7_CLOSING_TAG_RE` / `HTML_BLOCK_TYPE_6_RE` / type 1 regex
|
|
81
|
+
|
|
82
|
+
**Test**: `spec/whitespace_strictness_spec.rb` — "raw HTML tag whitespace
|
|
83
|
+
(CommonMark 6.6)"
|
|
84
|
+
|
|
85
|
+
### 2.3 Inline link tail separators limited to space/tab/at most 1 LF
|
|
86
|
+
|
|
87
|
+
**Spec**: §6.3 — the link tail (inside `(dest "title")`) is separated by "spaces,
|
|
88
|
+
tabs, and up to one line ending". FF / VT are not mentioned.
|
|
89
|
+
|
|
90
|
+
**RedQuilt behavior**: within the link tail, only space / tab are separators, and
|
|
91
|
+
a line ending is counted separately, up to one. If FF / VT appears, it does not
|
|
92
|
+
form a link (it is treated as normal paragraph text).
|
|
93
|
+
|
|
94
|
+
**Implementation**: `lib/red_quilt/inline/link_scanner.rb` —
|
|
95
|
+
`link_tail_whitespace_byte?`, `skip_link_whitespace`, `inline_link`,
|
|
96
|
+
`parse_link_title`
|
|
97
|
+
|
|
98
|
+
**Test**: `spec/whitespace_strictness_spec.rb` — "inline link tail whitespace
|
|
99
|
+
(CommonMark 6.3)"
|
|
100
|
+
|
|
101
|
+
### 2.4 Reference definition raw destination validated the same as inline links
|
|
102
|
+
|
|
103
|
+
**Spec**: §6.3 — the raw form of a link destination is "a nonempty sequence of
|
|
104
|
+
characters that does not start with `<`, does not include ASCII control
|
|
105
|
+
characters or space character, and includes parentheses only if (a) they are
|
|
106
|
+
backslash-escaped or (b) they are part of a balanced pair of unescaped
|
|
107
|
+
parentheses".
|
|
108
|
+
|
|
109
|
+
**RedQuilt behavior**: validates all of the above for the raw destination of a
|
|
110
|
+
reference definition too. Specifically, it rejects ASCII control
|
|
111
|
+
(U+0000–U+001F) / U+007F (DEL) / space, and tracks the depth of unescaped
|
|
112
|
+
parens, invalidating the definition if they are unbalanced.
|
|
113
|
+
|
|
114
|
+
**Past behavior**: it accepted destinations with a simple `/\A(\S+)(.*)\z/`, so
|
|
115
|
+
`[x]: foo(bar` or `[x]: foo\bbar` were also accepted as definitions.
|
|
116
|
+
|
|
117
|
+
**Implementation**: `lib/red_quilt/reference_definition.rb` —
|
|
118
|
+
`parse_raw_destination`, `RAW_DEST_FORBIDDEN_RE`
|
|
119
|
+
|
|
120
|
+
**Test**: `spec/link_validation_spec.rb` — "reference definition raw destination
|
|
121
|
+
validation"
|
|
122
|
+
|
|
123
|
+
### 2.5 Apply the 999-character link label limit on all paths
|
|
124
|
+
|
|
125
|
+
**Spec**: §6.3 — "A link label can have at most 999 characters inside the square
|
|
126
|
+
brackets."
|
|
127
|
+
|
|
128
|
+
**RedQuilt behavior**: rejects more than 999 characters on both the reference
|
|
129
|
+
definition side and the reference link usage side (shortcut / collapsed / full,
|
|
130
|
+
all of them).
|
|
131
|
+
|
|
132
|
+
**Implementation**:
|
|
133
|
+
- Constant: `lib/red_quilt/reference_definition.rb` —
|
|
134
|
+
`LABEL_MAX_LENGTH = 999`, the `label_too_long?` helper
|
|
135
|
+
- Definition side: `match_label` (decides for both single-line and multi-line)
|
|
136
|
+
- Usage side: `lib/red_quilt/inline/builder.rb` — `try_reference_link`,
|
|
137
|
+
`lib/red_quilt/inline/link_scanner.rb` — `reference_label`
|
|
138
|
+
|
|
139
|
+
**Test**: `spec/link_validation_spec.rb` — "link label length limit (999
|
|
140
|
+
characters)"
|
|
141
|
+
|
|
142
|
+
### 2.6 NCR digit limits and U+FFFD replacement of invalid codepoints
|
|
143
|
+
|
|
144
|
+
**Spec**: §6.4 — a decimal NCR is 1–7 digits, a hex NCR is 1–6 digits. If the
|
|
145
|
+
decode result is U+0000, a surrogate (U+D800–U+DFFF), or out of the Unicode range
|
|
146
|
+
(> U+10FFFF), it is replaced with U+FFFD.
|
|
147
|
+
|
|
148
|
+
**RedQuilt behavior**: implements all of the above.
|
|
149
|
+
|
|
150
|
+
**Past behavior**: it delegated to `CGI.unescapeHTML`, so an 8-digit decimal like
|
|
151
|
+
`A` or a surrogate like `�` would each decode to "A" or raise a
|
|
152
|
+
`RangeError`.
|
|
153
|
+
|
|
154
|
+
**Implementation**: `lib/red_quilt/inline/html_entities.rb` —
|
|
155
|
+
`Inline.decode_entity`, `Inline::ENTITY_RE`, `decode_numeric_codepoint`. The
|
|
156
|
+
`SURROGATE_RANGE` and `MAX_UNICODE_CODEPOINT` constants.
|
|
157
|
+
|
|
158
|
+
**Test**: `spec/numeric_character_reference_spec.rb`
|
|
159
|
+
|
|
160
|
+
### 2.7 GFM table header / delimiter cell-count match requirement
|
|
161
|
+
|
|
162
|
+
**Spec (GFM §4.10)**: "The header row must match the delimiter row in the number
|
|
163
|
+
of cells. If not, a table will not be recognized."
|
|
164
|
+
|
|
165
|
+
**RedQuilt behavior**: if the cell count of the header and the delimiter do not
|
|
166
|
+
match, it is not recognized as a table and is treated as a paragraph.
|
|
167
|
+
|
|
168
|
+
**Implementation**: `lib/red_quilt/block_parser.rb` — `table_start?`
|
|
169
|
+
|
|
170
|
+
**Test**: `spec/red_quilt_spec.rb` — "table separator validation (GFM spec)"
|
|
171
|
+
|
|
172
|
+
### 2.8 GFM extended autolink domain underscore constraint
|
|
173
|
+
|
|
174
|
+
**Spec (GFM §6.9)**: "If the domain name contains an underscore (`_`) in its last
|
|
175
|
+
two segments, it is invalid."
|
|
176
|
+
|
|
177
|
+
**RedQuilt behavior**: when extended autolinks are enabled, a URL / email whose
|
|
178
|
+
domain has `_` in its last two segments is not linkified.
|
|
179
|
+
|
|
180
|
+
**Implementation**: `lib/red_quilt/extended_autolink_pass.rb` — `valid_domain?` /
|
|
181
|
+
`extract_domain`
|
|
182
|
+
|
|
183
|
+
**Test**: `spec/extended_autolink_spec.rb` — "domain validation (GFM spec)"
|
|
184
|
+
|
|
185
|
+
## 3. Features outside the spec
|
|
186
|
+
|
|
187
|
+
Features not defined in the spec that RedQuilt provides for safety and
|
|
188
|
+
convenience.
|
|
189
|
+
|
|
190
|
+
### 3.1 Sanitizing unsafe URL schemes
|
|
191
|
+
|
|
192
|
+
**RedQuilt behavior**: if the scheme of a link / image destination is not in the
|
|
193
|
+
safe list below, it outputs `href` / `src` as an empty string. At the same time
|
|
194
|
+
it emits an `:unsafe_url` diagnostic as a warning. For CommonMark autolinks
|
|
195
|
+
(`<scheme:...>`), to stay spec-conformant, a denylist is used instead of a safe
|
|
196
|
+
list, and only schemes that could lead to script execution get an empty href.
|
|
197
|
+
|
|
198
|
+
**Safe schemes**: `http`, `https`, `mailto`, `ftp`, `tel`, `ssh`
|
|
199
|
+
|
|
200
|
+
**Schemes blocked in autolinks**: `javascript`, `vbscript`, `data`
|
|
201
|
+
|
|
202
|
+
**Implementation**: `lib/red_quilt/inline/builder.rb` — `SAFE_SCHEMES`,
|
|
203
|
+
`UNSAFE_AUTOLINK_SCHEMES`, `sanitize_destination`, `block_unsafe_autolink`
|
|
204
|
+
|
|
205
|
+
**Test**: `spec/red_quilt_spec.rb` — "sanitizes unsafe URL schemes"
|
|
206
|
+
|
|
207
|
+
### 3.2 Diagnostics
|
|
208
|
+
|
|
209
|
+
**RedQuilt behavior**: suspicious syntax, missing references, and potential
|
|
210
|
+
security events detected during parse / render are accumulated in
|
|
211
|
+
`Document#diagnostics` as `RedQuilt::Diagnostic` objects. Processing is never
|
|
212
|
+
interrupted (a tree and HTML are always returned).
|
|
213
|
+
|
|
214
|
+
**Rules currently emitted**:
|
|
215
|
+
|
|
216
|
+
| Rule | Severity | Description |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `:missing_reference` | warning | A full reference link `[text][ref]` has no definition. |
|
|
219
|
+
| `:duplicate_reference` | warning | There were multiple reference definitions with the same label (the first one is used). |
|
|
220
|
+
| `:duplicate_footnote` | warning | There were multiple footnote definitions with the same label (the first one is used; only when `footnotes: true`). |
|
|
221
|
+
| `:unsafe_url` | warning | An unsafe URL was replaced with an empty `href` / `src`. |
|
|
222
|
+
| `:empty_link` | warning | The link destination is empty (only when `lint: true`). |
|
|
223
|
+
| `:missing_alt` | info | An image's alt text is empty (only when `lint: true`). |
|
|
224
|
+
| `:heading_level_skip` | info | A heading level jumped by more than one (only when `lint: true`). |
|
|
225
|
+
|
|
226
|
+
**Implementation**: `lib/red_quilt/diagnostic.rb` (value object),
|
|
227
|
+
`lib/red_quilt/block_parser.rb` (duplicate reference),
|
|
228
|
+
`lib/red_quilt/footnote_definition.rb` (duplicate footnote),
|
|
229
|
+
`lib/red_quilt/inline/builder.rb` (missing / unsafe),
|
|
230
|
+
`lib/red_quilt/lint_pass.rb` (lint rules)
|
|
231
|
+
|
|
232
|
+
### 3.3 `allow_html` / `disallow_raw_html` flags
|
|
233
|
+
|
|
234
|
+
**RedQuilt behavior**:
|
|
235
|
+
|
|
236
|
+
| Flag | Default | Effect |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| `allow_html` | `false` | When false, raw HTML is fully escaped (turned into `<`). When true, HTML blocks and inline raw HTML are output as-is. |
|
|
239
|
+
| `disallow_raw_html` | `false` | The GFM "Disallowed Raw HTML" extension, enabled under `allow_html: true`. It rewrites `<` to `<` for the specified tags. |
|
|
240
|
+
|
|
241
|
+
The disallowed tag set defined by GFM: `title`, `textarea`, `style`, `xmp`,
|
|
242
|
+
`iframe`, `noembed`, `noframes`, `script`, `plaintext`
|
|
243
|
+
|
|
244
|
+
**Implementation**: `lib/red_quilt/document.rb` — `allow_html?` /
|
|
245
|
+
`disallow_raw_html?`
|
|
246
|
+
**Implementation (filter)**: `lib/red_quilt/renderer/html.rb` —
|
|
247
|
+
`DISALLOWED_RAW_TAGS` / `DISALLOWED_RAW_TAG_RE` / `filter_disallowed_raw`
|
|
248
|
+
|
|
249
|
+
## 4. Enabled extensions
|
|
250
|
+
|
|
251
|
+
### 4.1 GFM Table
|
|
252
|
+
|
|
253
|
+
Always enabled. In addition to the spec, the column-count match requirement from
|
|
254
|
+
2.7 is applied.
|
|
255
|
+
|
|
256
|
+
**Implementation**: `lib/red_quilt/block_parser.rb` — `table_start?` /
|
|
257
|
+
`parse_table`
|
|
258
|
+
|
|
259
|
+
### 4.2 GFM Strikethrough
|
|
260
|
+
|
|
261
|
+
Always enabled. Only the double tilde `~~text~~` is supported (matching GFM
|
|
262
|
+
behavior). A single tilde `~text~` is treated as normal text.
|
|
263
|
+
|
|
264
|
+
**Implementation**: `lib/red_quilt/inline/lexer.rb` (handling of `~` in
|
|
265
|
+
`SPECIAL_BYTES` and `scan_delim_run`), `lib/red_quilt/inline/builder.rb`
|
|
266
|
+
(generating `STRIKETHROUGH` in `process_emphasis`)
|
|
267
|
+
|
|
268
|
+
### 4.3 GFM Disallowed Raw HTML
|
|
269
|
+
|
|
270
|
+
Opt-in. It only works when `allow_html: true, disallow_raw_html: true` are used
|
|
271
|
+
together (under `allow_html: false` all HTML is escaped, so it has no effect).
|
|
272
|
+
See 3.3 for details.
|
|
273
|
+
|
|
274
|
+
### 4.4 GFM Extended Autolink
|
|
275
|
+
|
|
276
|
+
Opt-in. Specifying `extended_autolinks: true` runs `ExtendedAutolinkPass` as a
|
|
277
|
+
pass that linkifies bare URLs / emails / `www.`-prefixed strings that are not
|
|
278
|
+
wrapped in `<...>`.
|
|
279
|
+
|
|
280
|
+
**Additional constraint**: implements the domain underscore check from 2.8.
|
|
281
|
+
|
|
282
|
+
**Implementation**: `lib/red_quilt/extended_autolink_pass.rb`
|
|
283
|
+
|
|
284
|
+
### 4.5 GFM Footnotes
|
|
285
|
+
|
|
286
|
+
Opt-in. Specifying `footnotes: true` removes `[^label]: ...` definitions from the
|
|
287
|
+
body flow and converts `[^label]` references into sup links. Only the referenced
|
|
288
|
+
definitions are kept, ordered by first reference, and output as a
|
|
289
|
+
`FOOTNOTES_SECTION` at the end of the root. Unreferenced definitions are not
|
|
290
|
+
output.
|
|
291
|
+
|
|
292
|
+
**Implementation**: `lib/red_quilt/footnote_definition.rb`,
|
|
293
|
+
`lib/red_quilt/footnote_registry.rb`, `lib/red_quilt/footnote_pass.rb`
|
|
294
|
+
|
|
295
|
+
## 5. Unsupported / known limitations
|
|
296
|
+
|
|
297
|
+
- GFM Task List Items (`- [ ]` / `- [x]`) are not supported. They are parsed as
|
|
298
|
+
normal list items.
|
|
299
|
+
|
|
300
|
+
## 6. Correspondence with tests
|
|
301
|
+
|
|
302
|
+
This section collects the spec files that verify the difference items.
|
|
303
|
+
|
|
304
|
+
| Aspect | Spec file |
|
|
305
|
+
|---|---|
|
|
306
|
+
| Passing the official CommonMark examples | `spec/commonmark_compat_spec.rb` |
|
|
307
|
+
| Input normalization (line endings / NUL / blank line) | `spec/input_normalization_spec.rb` |
|
|
308
|
+
| Whitespace strictness (autolink / raw HTML / link tail) | `spec/whitespace_strictness_spec.rb` |
|
|
309
|
+
| Link / reference validation (label cap / raw dest) | `spec/link_validation_spec.rb` |
|
|
310
|
+
| NCR digit limits and invalid codepoints | `spec/numeric_character_reference_spec.rb` |
|
|
311
|
+
| GFM table column-count match | `spec/red_quilt_spec.rb` — "table separator validation" |
|
|
312
|
+
| GFM extended autolink domain validation | `spec/extended_autolink_spec.rb` |
|
|
313
|
+
| GFM footnotes | `spec/footnotes_spec.rb` |
|
|
314
|
+
| URL scheme sanitization | `spec/red_quilt_spec.rb` — "sanitizes unsafe URL schemes" |
|
|
315
|
+
| Diagnostics / lint diagnostics | `spec/diagnostic_spec.rb` |
|
|
316
|
+
| Disallowed Raw HTML | `spec/red_quilt_spec.rb` — disallow_raw_html cases |
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module RedQuilt
|
|
6
|
+
# Opens a local HTML file in the OS default browser when `--open` is set.
|
|
7
|
+
# Best-effort: unsupported platforms or spawn failures are logged but
|
|
8
|
+
# never abort the CLI.
|
|
9
|
+
class BrowserLauncher
|
|
10
|
+
def initialize(err:)
|
|
11
|
+
@err = err
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def launch(path)
|
|
15
|
+
command = platform_command
|
|
16
|
+
unless command
|
|
17
|
+
@err.puts "redquilt: --open is not supported on this platform; skipping."
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
pid = Process.spawn(*command, path, in: :close, out: File::NULL, err: File::NULL)
|
|
22
|
+
Process.detach(pid)
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
@err.puts "redquilt: failed to open browser: #{e.message}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def platform_command
|
|
30
|
+
case RbConfig::CONFIG["host_os"]
|
|
31
|
+
when /darwin/ then ["open"]
|
|
32
|
+
when /linux|bsd/ then ["xdg-open"]
|
|
33
|
+
when /mswin|mingw|cygwin/ then ["cmd.exe", "/c", "start", ""]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/red_quilt/cli.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
+
require "tmpdir"
|
|
4
5
|
require "red_quilt"
|
|
6
|
+
require "red_quilt/browser_launcher"
|
|
5
7
|
|
|
6
8
|
module RedQuilt
|
|
7
9
|
# Entry point for the `redquilt` executable. Defined as a module-level
|
|
@@ -34,6 +36,9 @@ module RedQuilt
|
|
|
34
36
|
lang: "en",
|
|
35
37
|
css: nil,
|
|
36
38
|
theme: :default,
|
|
39
|
+
output: nil,
|
|
40
|
+
open: false,
|
|
41
|
+
mermaid: false,
|
|
37
42
|
}.freeze
|
|
38
43
|
|
|
39
44
|
THEMES = %i[none default].freeze
|
|
@@ -44,6 +49,13 @@ module RedQuilt
|
|
|
44
49
|
options = parse_options(argv, stderr: stderr)
|
|
45
50
|
return options if options.is_a?(Integer)
|
|
46
51
|
|
|
52
|
+
if options[:open] && options[:format] != :html
|
|
53
|
+
stderr.puts "redquilt: --open requires --format html"
|
|
54
|
+
return 1
|
|
55
|
+
end
|
|
56
|
+
options[:standalone] = true if options[:open]
|
|
57
|
+
|
|
58
|
+
source_path = argv.first
|
|
47
59
|
source = read_source(argv, stdin: stdin, stderr: stderr)
|
|
48
60
|
return 1 unless source
|
|
49
61
|
|
|
@@ -54,15 +66,7 @@ module RedQuilt
|
|
|
54
66
|
lint: options[:lint])
|
|
55
67
|
|
|
56
68
|
unless options[:diagnostics_only]
|
|
57
|
-
|
|
58
|
-
when :html
|
|
59
|
-
stdout.write(render_html(doc, options))
|
|
60
|
-
when :ast
|
|
61
|
-
require "pp"
|
|
62
|
-
PP.pp(doc.to_ast, stdout)
|
|
63
|
-
when :json
|
|
64
|
-
stdout.puts doc.to_json
|
|
65
|
-
end
|
|
69
|
+
emit_output(doc, options, source_path: source_path, stdout: stdout, stderr: stderr)
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
if options[:diagnostics] || options[:diagnostics_only]
|
|
@@ -72,6 +76,35 @@ module RedQuilt
|
|
|
72
76
|
doc.diagnostics.any? { |d| d.severity == :error } ? 1 : 0
|
|
73
77
|
end
|
|
74
78
|
|
|
79
|
+
def self.emit_output(doc, options, source_path:, stdout:, stderr:)
|
|
80
|
+
destination = output_destination(options, source_path)
|
|
81
|
+
|
|
82
|
+
case options[:format]
|
|
83
|
+
when :html
|
|
84
|
+
html = render_html(doc, options)
|
|
85
|
+
if destination
|
|
86
|
+
File.write(destination, html)
|
|
87
|
+
else
|
|
88
|
+
stdout.write(html)
|
|
89
|
+
end
|
|
90
|
+
when :ast
|
|
91
|
+
require "pp"
|
|
92
|
+
PP.pp(doc.to_ast, stdout)
|
|
93
|
+
when :json
|
|
94
|
+
stdout.puts doc.to_json
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
BrowserLauncher.new(err: stderr).launch(destination) if options[:open] && destination
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.output_destination(options, source_path)
|
|
101
|
+
return options[:output] if options[:output]
|
|
102
|
+
return nil unless options[:open]
|
|
103
|
+
|
|
104
|
+
base = source_path ? File.basename(source_path, ".*") : "stdin"
|
|
105
|
+
File.join(Dir.tmpdir, "redquilt-#{base}.html")
|
|
106
|
+
end
|
|
107
|
+
|
|
75
108
|
def self.parse_options(argv, stderr:)
|
|
76
109
|
options = DEFAULTS.dup
|
|
77
110
|
parser = OptionParser.new do |opts|
|
|
@@ -115,6 +148,17 @@ module RedQuilt
|
|
|
115
148
|
"Embedded stylesheet: default (the default) or none (bare HTML)") do |t|
|
|
116
149
|
options[:theme] = t
|
|
117
150
|
end
|
|
151
|
+
opts.on("-o", "--output FILE", "Write HTML to FILE instead of stdout") do |f|
|
|
152
|
+
options[:output] = f
|
|
153
|
+
end
|
|
154
|
+
opts.on("--open",
|
|
155
|
+
"Write HTML to a file and open it in the default browser (forces --standalone)") do
|
|
156
|
+
options[:open] = true
|
|
157
|
+
end
|
|
158
|
+
opts.on("--mermaid",
|
|
159
|
+
"Render `mermaid` code blocks as diagrams (loads mermaid.js from a CDN in standalone output)") do
|
|
160
|
+
options[:mermaid] = true
|
|
161
|
+
end
|
|
118
162
|
opts.on("--diagnostics", "Also print diagnostics to stderr") do
|
|
119
163
|
options[:diagnostics] = true
|
|
120
164
|
end
|
|
@@ -167,6 +211,7 @@ module RedQuilt
|
|
|
167
211
|
lang: options[:lang],
|
|
168
212
|
css: options[:css],
|
|
169
213
|
theme: options[:theme],
|
|
214
|
+
mermaid: options[:mermaid],
|
|
170
215
|
)
|
|
171
216
|
end
|
|
172
217
|
|
data/lib/red_quilt/document.rb
CHANGED
|
@@ -47,11 +47,15 @@ module RedQuilt
|
|
|
47
47
|
# (an external stylesheet link) is independent and may be combined.
|
|
48
48
|
# heading_ids: when true, every heading gets a slugified `id` (Unicode
|
|
49
49
|
# preserving, deduplicated within the document) for anchor links.
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
# mermaid: when true, fenced code blocks tagged `mermaid` render as
|
|
51
|
+
# `<pre class="mermaid">` containers instead of `<pre><code>`. In
|
|
52
|
+
# standalone mode the mermaid.js runtime is also loaded from a CDN so
|
|
53
|
+
# the diagrams render in the browser without further setup.
|
|
54
|
+
def to_html(standalone: false, title: nil, lang: "en", css: nil, theme: :none, heading_ids: false, mermaid: false)
|
|
55
|
+
body = Renderer::HTML.new(self, heading_ids: heading_ids, mermaid: mermaid).render
|
|
52
56
|
return body unless standalone
|
|
53
57
|
|
|
54
|
-
wrap_standalone_html(body, title: title.to_s, lang: lang.to_s, css: css, theme: Theme.css(theme))
|
|
58
|
+
wrap_standalone_html(body, title: title.to_s, lang: lang.to_s, css: css, theme: Theme.css(theme), mermaid: mermaid)
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
def to_ast
|
|
@@ -87,7 +91,68 @@ module RedQuilt
|
|
|
87
91
|
|
|
88
92
|
private
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
# Self-contained assets embedded in standalone output when mermaid
|
|
95
|
+
# support is enabled. Loads the mermaid.js runtime from a CDN as an ES
|
|
96
|
+
# module, renders every `<pre class="mermaid">` container, then makes
|
|
97
|
+
# each diagram interactive with svg-pan-zoom (also from a CDN): mouse
|
|
98
|
+
# wheel zooms, drag pans, and a small control panel offers +/-/reset.
|
|
99
|
+
MERMAID_SCRIPT = <<~HTML
|
|
100
|
+
<style>
|
|
101
|
+
.rq-mermaid-pz {
|
|
102
|
+
/* Break out of the body's max-width column so the viewport isn't a
|
|
103
|
+
narrow peephole: span most of the viewport width, centered. */
|
|
104
|
+
width: 80vw;
|
|
105
|
+
margin-left: calc(50% - 40vw);
|
|
106
|
+
height: 80vh;
|
|
107
|
+
border: 1px solid #d0d7de;
|
|
108
|
+
border-radius: 6px;
|
|
109
|
+
overflow: hidden;
|
|
110
|
+
}
|
|
111
|
+
.rq-mermaid-pz svg {
|
|
112
|
+
width: 100%;
|
|
113
|
+
height: 100%;
|
|
114
|
+
max-width: none;
|
|
115
|
+
display: block;
|
|
116
|
+
cursor: grab;
|
|
117
|
+
}
|
|
118
|
+
@media (prefers-color-scheme: dark) {
|
|
119
|
+
.rq-mermaid-pz { border-color: #30363d; }
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
122
|
+
<script type="module">
|
|
123
|
+
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs";
|
|
124
|
+
import svgPanZoom from "https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/+esm";
|
|
125
|
+
mermaid.initialize({ startOnLoad: false });
|
|
126
|
+
await mermaid.run();
|
|
127
|
+
|
|
128
|
+
for (const pre of document.querySelectorAll("pre.mermaid")) {
|
|
129
|
+
const svg = pre.querySelector("svg");
|
|
130
|
+
if (!svg) continue;
|
|
131
|
+
// Drop mermaid's inline max-width and let the SVG fill a sized box so
|
|
132
|
+
// svg-pan-zoom has room to zoom/pan. The whole viewBox scales as one,
|
|
133
|
+
// so every element stays aligned.
|
|
134
|
+
svg.removeAttribute("style");
|
|
135
|
+
svg.setAttribute("width", "100%");
|
|
136
|
+
svg.setAttribute("height", "100%");
|
|
137
|
+
const box = document.createElement("div");
|
|
138
|
+
box.className = "rq-mermaid-pz";
|
|
139
|
+
pre.replaceWith(box);
|
|
140
|
+
box.appendChild(svg);
|
|
141
|
+
svgPanZoom(svg, {
|
|
142
|
+
zoomEnabled: true,
|
|
143
|
+
controlIconsEnabled: true,
|
|
144
|
+
fit: true,
|
|
145
|
+
center: true,
|
|
146
|
+
zoomScaleSensitivity: 0.3,
|
|
147
|
+
minZoom: 0.2,
|
|
148
|
+
maxZoom: 20,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
</script>
|
|
152
|
+
HTML
|
|
153
|
+
private_constant :MERMAID_SCRIPT
|
|
154
|
+
|
|
155
|
+
def wrap_standalone_html(body, title:, lang:, css:, theme:, mermaid: false)
|
|
91
156
|
out = +"<!DOCTYPE html>\n"
|
|
92
157
|
out << %(<html lang="#{html_escape_attr(lang)}">\n)
|
|
93
158
|
out << "<head>\n"
|
|
@@ -97,6 +162,7 @@ module RedQuilt
|
|
|
97
162
|
out << "<style>\n#{theme}</style>\n" if theme
|
|
98
163
|
out << "</head>\n<body>\n"
|
|
99
164
|
out << body
|
|
165
|
+
out << MERMAID_SCRIPT if mermaid
|
|
100
166
|
out << "</body>\n</html>\n"
|
|
101
167
|
out
|
|
102
168
|
end
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
module RedQuilt
|
|
4
4
|
module Renderer
|
|
5
5
|
class HTML
|
|
6
|
-
def initialize(document, heading_ids: false)
|
|
6
|
+
def initialize(document, heading_ids: false, mermaid: false)
|
|
7
7
|
@document = document
|
|
8
8
|
@arena = document.arena
|
|
9
9
|
@out = +""
|
|
10
10
|
@slugger = Slug::Counter.new if heading_ids
|
|
11
|
+
@mermaid = mermaid
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def render
|
|
@@ -73,12 +74,21 @@ module RedQuilt
|
|
|
73
74
|
render_list_item(node_id)
|
|
74
75
|
@out << "</li>\n"
|
|
75
76
|
when NodeType::CODE_BLOCK
|
|
76
|
-
@out << "<pre><code"
|
|
77
77
|
info_word = @arena.str2(node_id).to_s.split.first.to_s
|
|
78
|
-
@
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
if @mermaid && info_word == "mermaid"
|
|
79
|
+
# Emit a container mermaid.js recognizes via class="mermaid".
|
|
80
|
+
# The diagram source is still HTML-escaped; the browser decodes
|
|
81
|
+
# the entities back into textContent, which is what mermaid reads.
|
|
82
|
+
@out << %(<pre class="mermaid">)
|
|
83
|
+
@out << escape_html(@arena.text(node_id).to_s)
|
|
84
|
+
@out << "</pre>\n"
|
|
85
|
+
else
|
|
86
|
+
@out << "<pre><code"
|
|
87
|
+
@out << %( class="language-#{escape_html(info_word)}") unless info_word.empty?
|
|
88
|
+
@out << ">"
|
|
89
|
+
@out << escape_html(@arena.text(node_id).to_s)
|
|
90
|
+
@out << "</code></pre>\n"
|
|
91
|
+
end
|
|
82
92
|
when NodeType::HTML_BLOCK
|
|
83
93
|
render_raw_html(@arena.text(node_id).to_s, block: true)
|
|
84
94
|
when NodeType::TABLE
|
data/lib/red_quilt/version.rb
CHANGED
data/lib/red_quilt.rb
CHANGED
|
@@ -57,13 +57,13 @@ module RedQuilt
|
|
|
57
57
|
document
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
def render_html(source, allow_html: false, disallow_raw_html: false, extended_autolinks: false, footnotes: false, lint: false, heading_ids: false)
|
|
60
|
+
def render_html(source, allow_html: false, disallow_raw_html: false, extended_autolinks: false, footnotes: false, lint: false, heading_ids: false, mermaid: false)
|
|
61
61
|
parse(source,
|
|
62
62
|
allow_html: allow_html,
|
|
63
63
|
disallow_raw_html: disallow_raw_html,
|
|
64
64
|
extended_autolinks: extended_autolinks,
|
|
65
65
|
footnotes: footnotes,
|
|
66
|
-
lint: lint).to_html(heading_ids: heading_ids)
|
|
66
|
+
lint: lint).to_html(heading_ids: heading_ids, mermaid: mermaid)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
private
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: red_quilt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- takahashim
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: A modern Markdown document processor in pure Ruby, with an arena-style
|
|
13
13
|
AST and full CommonMark spec test suite compliance.
|
|
@@ -26,13 +26,17 @@ files:
|
|
|
26
26
|
- Rakefile
|
|
27
27
|
- docs/api.md
|
|
28
28
|
- docs/architecture.ja.md
|
|
29
|
+
- docs/architecture.md
|
|
29
30
|
- docs/arena-usage.ja.md
|
|
31
|
+
- docs/arena-usage.md
|
|
30
32
|
- docs/commonmark-conformance.ja.md
|
|
33
|
+
- docs/commonmark-conformance.md
|
|
31
34
|
- exe/redquilt
|
|
32
35
|
- lib/red_quilt.rb
|
|
33
36
|
- lib/red_quilt/arena.rb
|
|
34
37
|
- lib/red_quilt/block_parser.rb
|
|
35
38
|
- lib/red_quilt/blockquote.rb
|
|
39
|
+
- lib/red_quilt/browser_launcher.rb
|
|
36
40
|
- lib/red_quilt/cli.rb
|
|
37
41
|
- lib/red_quilt/diagnostic.rb
|
|
38
42
|
- lib/red_quilt/document.rb
|
|
@@ -90,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
90
94
|
- !ruby/object:Gem::Version
|
|
91
95
|
version: '0'
|
|
92
96
|
requirements: []
|
|
93
|
-
rubygems_version: 3.6.
|
|
97
|
+
rubygems_version: 3.6.9
|
|
94
98
|
specification_version: 4
|
|
95
99
|
summary: CommonMark-based Markdown processor written in pure Ruby
|
|
96
100
|
test_files: []
|