@perspective-dev/viewer 5.1.0 → 5.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.
Files changed (175) hide show
  1. package/dist/cdn/perspective-viewer.js +9 -9
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/intl/de.css +1 -1
  8. package/dist/css/intl/es.css +1 -1
  9. package/dist/css/intl/fr.css +1 -1
  10. package/dist/css/intl/ja.css +1 -1
  11. package/dist/css/intl/pt.css +1 -1
  12. package/dist/css/intl/zh.css +1 -1
  13. package/dist/css/intl.css +1 -1
  14. package/dist/css/monokai.css +1 -1
  15. package/dist/css/phosphor.css +1 -1
  16. package/dist/css/pro-dark.css +1 -1
  17. package/dist/css/pro.css +1 -1
  18. package/dist/css/solarized-dark.css +1 -1
  19. package/dist/css/solarized.css +1 -1
  20. package/dist/css/themes.css +1 -1
  21. package/dist/css/vaporwave.css +1 -1
  22. package/dist/docs/perspective-docs.json +1 -0
  23. package/dist/esm/column-format.d.ts +19 -28
  24. package/dist/esm/perspective-viewer.d.ts +2 -0
  25. package/dist/esm/perspective-viewer.inline.js +8 -8
  26. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  27. package/dist/esm/perspective-viewer.js +9 -9
  28. package/dist/esm/perspective-viewer.js.map +4 -4
  29. package/dist/esm/providers.d.ts +79 -0
  30. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  31. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  32. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  33. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  34. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  35. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  36. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  37. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  38. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  39. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  40. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  41. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  42. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  43. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  44. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +41 -1
  45. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  46. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  47. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  48. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  49. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  50. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  51. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  52. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  53. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  54. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  55. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  56. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  57. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  58. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  59. package/dist/esm/ts-rs/ViewerConfig.d.ts +41 -1
  60. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  61. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +63 -0
  62. package/dist/esm/ts-rs/WindowSpec.d.ts +3 -2
  63. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
  64. package/dist/wasm/perspective-viewer.d.ts +134 -25
  65. package/dist/wasm/perspective-viewer.js +367 -124
  66. package/dist/wasm/perspective-viewer.wasm +0 -0
  67. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
  68. package/package.json +2 -1
  69. package/src/css/_viewer_bundle.css +1 -0
  70. package/src/css/column-selector.css +8 -0
  71. package/src/css/config-selector.css +38 -3
  72. package/src/css/form/code-editor.css +2 -3
  73. package/src/css/form/mirrored-textarea.css +81 -0
  74. package/src/css/viewer.css +328 -0
  75. package/src/rust/agent/client/mod.rs +19 -0
  76. package/src/rust/agent/client/protocol.rs +320 -0
  77. package/src/rust/agent/client/transport.rs +287 -0
  78. package/src/rust/agent/client/turn.rs +174 -0
  79. package/src/rust/agent/config.rs +159 -0
  80. package/src/rust/agent/docs.rs +293 -0
  81. package/src/rust/agent/markdown/block.rs +509 -0
  82. package/src/rust/agent/markdown/entity.rs +52 -0
  83. package/src/rust/agent/markdown/events.rs +71 -0
  84. package/src/rust/agent/markdown/inline.rs +497 -0
  85. package/src/rust/agent/markdown/mod.rs +466 -0
  86. package/src/rust/agent/mod.rs +23 -0
  87. package/src/rust/agent/preamble.md +16 -0
  88. package/src/rust/agent/runtime.rs +201 -0
  89. package/src/rust/agent/slot.rs +236 -0
  90. package/src/rust/agent/tools.rs +1110 -0
  91. package/src/rust/components/chat_panel.rs +289 -0
  92. package/src/rust/components/column_selector/config_selector.rs +76 -7
  93. package/src/rust/components/column_selector.rs +7 -0
  94. package/src/rust/components/containers/dragdrop_list.rs +37 -0
  95. package/src/rust/components/form/code_editor.rs +14 -23
  96. package/src/rust/components/form/debug.rs +8 -5
  97. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  98. package/src/rust/components/form/mod.rs +1 -0
  99. package/src/rust/components/mod.rs +2 -0
  100. package/src/rust/components/settings_panel.rs +80 -2
  101. package/src/rust/components/viewer/panels.rs +9 -14
  102. package/src/rust/components/viewer/render.rs +2 -0
  103. package/src/rust/components/window_editor.rs +75 -103
  104. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  105. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  106. package/src/rust/config/number_string_format.rs +33 -0
  107. package/src/rust/config/options.rs +18 -0
  108. package/src/rust/config/plugin_static_config.rs +120 -5
  109. package/src/rust/config/viewer_config.rs +220 -1
  110. package/src/rust/config/workspace_config.rs +7 -6
  111. package/src/rust/custom_elements/viewer.rs +209 -18
  112. package/src/rust/custom_events.rs +29 -0
  113. package/src/rust/lib.rs +13 -0
  114. package/src/rust/presentation.rs +8 -0
  115. package/src/rust/queries/get_viewer_config.rs +5 -1
  116. package/src/rust/session/column_defaults_update.rs +18 -0
  117. package/src/rust/session/metadata.rs +18 -20
  118. package/src/rust/session/replace_expression_update.rs +1 -0
  119. package/src/rust/tasks/create_panel.rs +18 -8
  120. package/src/rust/tasks/edit_window.rs +1 -1
  121. package/src/rust/tasks/restore_panel.rs +34 -17
  122. package/src/rust/tasks/sync_update_panels.rs +7 -23
  123. package/src/rust/workspace.rs +112 -20
  124. package/src/themes/intl/de.css +16 -0
  125. package/src/themes/intl/es.css +16 -0
  126. package/src/themes/intl/fr.css +16 -0
  127. package/src/themes/intl/ja.css +16 -0
  128. package/src/themes/intl/pt.css +16 -0
  129. package/src/themes/intl/zh.css +16 -0
  130. package/src/themes/intl.css +16 -0
  131. package/src/ts/column-format.ts +67 -46
  132. package/src/ts/perspective-viewer.ts +2 -0
  133. package/src/ts/providers.ts +96 -0
  134. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  135. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  136. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  137. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  138. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  139. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  140. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  141. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  142. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  143. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  144. package/src/ts/ts-rs/FormatMode.ts +3 -0
  145. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  146. package/src/ts/ts-rs/Notation.ts +11 -0
  147. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  148. package/src/ts/ts-rs/PanelViewerConfig.ts +47 -1
  149. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  150. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  151. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  152. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  153. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  154. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  155. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  156. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  157. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  158. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  159. package/src/ts/ts-rs/Unit.ts +3 -0
  160. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  161. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  162. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  163. package/src/ts/ts-rs/ViewerConfig.ts +47 -1
  164. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  165. package/src/ts/ts-rs/ViewerConfigUpdate.ts +72 -2
  166. package/src/ts/ts-rs/WindowSpec.ts +3 -2
  167. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
  168. package/dist/esm/ts-rs/WindowAggregate.d.ts +0 -1
  169. package/src/ts/ts-rs/WindowAggregate.ts +0 -3
  170. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
  171. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
  172. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
  173. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
  174. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
  175. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
@@ -0,0 +1,497 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ //! Inline markdown: code spans, emphasis/strong/strikethrough, links,
14
+ //! images, autolinks, entities and breaks. A hand-rolled scanner (delimiter
15
+ //! matching needs lookbehind and run-length state that combinators fit
16
+ //! poorly) over a simplified flanking rule: a run may open iff its next
17
+ //! char is non-whitespace and close iff its previous char is
18
+ //! non-whitespace, with `_` additionally barred from opening/closing
19
+ //! against an alphanumeric neighbor so `snake_case` identifiers in prose
20
+ //! stay literal. Unpaired delimiters degrade to their literal text.
21
+
22
+ use super::entity;
23
+ use super::events::{Event, Tag};
24
+
25
+ enum Inline {
26
+ Text(String),
27
+ Code(String),
28
+ SoftBreak,
29
+ HardBreak,
30
+ Emphasis(Vec<Inline>),
31
+ Strong(Vec<Inline>),
32
+ Strikethrough(Vec<Inline>),
33
+ Link {
34
+ dest_url: String,
35
+ title: String,
36
+ children: Vec<Inline>,
37
+ },
38
+ Image {
39
+ dest_url: String,
40
+ children: Vec<Inline>,
41
+ },
42
+ }
43
+
44
+ enum ScanItem {
45
+ Node(Inline),
46
+ Delim {
47
+ ch: char,
48
+ len: usize,
49
+ can_open: bool,
50
+ can_close: bool,
51
+ },
52
+ }
53
+
54
+ /// Parse `text` (one paragraph/heading/cell's worth, newlines = soft break
55
+ /// candidates) and append its events.
56
+ pub fn parse_into(text: &str, events: &mut Vec<Event>) {
57
+ emit(resolve(scan(text)), events);
58
+ }
59
+
60
+ fn scan(text: &str) -> Vec<ScanItem> {
61
+ let chars = text.chars().collect::<Vec<_>>();
62
+ let mut items = vec![];
63
+ let mut buf = String::new();
64
+ let mut i = 0;
65
+ macro_rules! flush {
66
+ () => {
67
+ if !buf.is_empty() {
68
+ items.push(ScanItem::Node(Inline::Text(std::mem::take(&mut buf))));
69
+ }
70
+ };
71
+ }
72
+
73
+ while i < chars.len() {
74
+ let c = chars[i];
75
+ match c {
76
+ '\\' if i + 1 < chars.len() => {
77
+ let next = chars[i + 1];
78
+ if next == '\n' {
79
+ flush!();
80
+ items.push(ScanItem::Node(Inline::HardBreak));
81
+ } else if next.is_ascii_punctuation() {
82
+ buf.push(next);
83
+ } else {
84
+ buf.push('\\');
85
+ buf.push(next);
86
+ }
87
+
88
+ i += 2;
89
+ },
90
+ '\n' => {
91
+ let hard = buf.ends_with(" ");
92
+ while buf.ends_with(' ') {
93
+ buf.pop();
94
+ }
95
+
96
+ flush!();
97
+ items.push(ScanItem::Node(if hard {
98
+ Inline::HardBreak
99
+ } else {
100
+ Inline::SoftBreak
101
+ }));
102
+
103
+ i += 1;
104
+ while chars.get(i) == Some(&' ') {
105
+ i += 1;
106
+ }
107
+ },
108
+ '`' => {
109
+ let run = run_len(&chars, i, '`');
110
+ if let Some(close) = find_code_close(&chars, i + run, run) {
111
+ flush!();
112
+ let content = chars[i + run..close]
113
+ .iter()
114
+ .map(|x| if *x == '\n' { ' ' } else { *x })
115
+ .collect::<String>();
116
+
117
+ items.push(ScanItem::Node(Inline::Code(trim_code_span(content))));
118
+ i = close + run;
119
+ } else {
120
+ buf.extend(std::iter::repeat_n('`', run));
121
+ i += run;
122
+ }
123
+ },
124
+ '*' | '_' | '~' => {
125
+ let run = run_len(&chars, i, c);
126
+ let prev = if i > 0 { Some(chars[i - 1]) } else { None };
127
+ let next = chars.get(i + run).copied();
128
+ let mut can_open = next.is_some_and(|x| !x.is_whitespace());
129
+ let mut can_close = prev.is_some_and(|x| !x.is_whitespace());
130
+ if c == '_' {
131
+ can_open &= !prev.is_some_and(|x| x.is_alphanumeric());
132
+ can_close &= !next.is_some_and(|x| x.is_alphanumeric());
133
+ }
134
+
135
+ if c == '~' && run < 2 || !can_open && !can_close {
136
+ buf.extend(std::iter::repeat_n(c, run));
137
+ } else {
138
+ flush!();
139
+ items.push(ScanItem::Delim {
140
+ ch: c,
141
+ len: run,
142
+ can_open,
143
+ can_close,
144
+ });
145
+ }
146
+
147
+ i += run;
148
+ },
149
+ '[' => match parse_link(&chars, i, false) {
150
+ Some((node, next)) => {
151
+ flush!();
152
+ items.push(ScanItem::Node(node));
153
+ i = next;
154
+ },
155
+ None => {
156
+ buf.push('[');
157
+ i += 1;
158
+ },
159
+ },
160
+ '!' if chars.get(i + 1) == Some(&'[') => match parse_link(&chars, i + 1, true) {
161
+ Some((node, next)) => {
162
+ flush!();
163
+ items.push(ScanItem::Node(node));
164
+ i = next;
165
+ },
166
+ None => {
167
+ buf.push('!');
168
+ i += 1;
169
+ },
170
+ },
171
+ '<' => match parse_autolink(&chars, i) {
172
+ Some((node, next)) => {
173
+ flush!();
174
+ items.push(ScanItem::Node(node));
175
+ i = next;
176
+ },
177
+ None => {
178
+ buf.push('<');
179
+ i += 1;
180
+ },
181
+ },
182
+ '&' => {
183
+ let rest = chars[i + 1..].iter().collect::<String>();
184
+ match entity::decode(&rest) {
185
+ Some((decoded, used)) => {
186
+ buf.push(decoded);
187
+ i += 1 + used;
188
+ },
189
+ None => {
190
+ buf.push('&');
191
+ i += 1;
192
+ },
193
+ }
194
+ },
195
+ c => {
196
+ buf.push(c);
197
+ i += 1;
198
+ },
199
+ }
200
+ }
201
+
202
+ flush!();
203
+ items
204
+ }
205
+
206
+ fn run_len(chars: &[char], start: usize, ch: char) -> usize {
207
+ chars[start..].iter().take_while(|x| **x == ch).count()
208
+ }
209
+
210
+ /// Find the next backtick run of EXACTLY `n` (longer/shorter runs are
211
+ /// skipped whole, per CommonMark).
212
+ fn find_code_close(chars: &[char], mut i: usize, n: usize) -> Option<usize> {
213
+ while i < chars.len() {
214
+ if chars[i] == '`' {
215
+ let run = run_len(chars, i, '`');
216
+ if run == n {
217
+ return Some(i);
218
+ }
219
+
220
+ i += run;
221
+ } else {
222
+ i += 1;
223
+ }
224
+ }
225
+
226
+ None
227
+ }
228
+
229
+ fn trim_code_span(content: String) -> String {
230
+ if content.starts_with(' ')
231
+ && content.ends_with(' ')
232
+ && content.len() > 1
233
+ && !content.chars().all(|x| x == ' ')
234
+ {
235
+ content[1..content.len() - 1].to_owned()
236
+ } else {
237
+ content
238
+ }
239
+ }
240
+
241
+ /// `[text](dest "title")` at `i` (pointing at the `[`). The bracket text is
242
+ /// parsed recursively; URL trust policy stays in the renderer.
243
+ fn parse_link(chars: &[char], i: usize, image: bool) -> Option<(Inline, usize)> {
244
+ let mut j = i + 1;
245
+ let mut depth = 1;
246
+ while j < chars.len() {
247
+ match chars[j] {
248
+ '\\' => j += 1,
249
+ '[' => depth += 1,
250
+ ']' => {
251
+ depth -= 1;
252
+ if depth == 0 {
253
+ break;
254
+ }
255
+ },
256
+ _ => (),
257
+ }
258
+
259
+ j += 1;
260
+ }
261
+
262
+ if depth != 0 || chars.get(j + 1) != Some(&'(') {
263
+ return None;
264
+ }
265
+
266
+ let mut k = j + 2;
267
+ while chars.get(k) == Some(&' ') {
268
+ k += 1;
269
+ }
270
+
271
+ let mut dest_url = String::new();
272
+ let mut paren_depth = 0;
273
+ while k < chars.len() {
274
+ match chars[k] {
275
+ '\\' if k + 1 < chars.len() => {
276
+ dest_url.push(chars[k + 1]);
277
+ k += 1;
278
+ },
279
+ '(' => {
280
+ paren_depth += 1;
281
+ dest_url.push('(');
282
+ },
283
+ ')' if paren_depth == 0 => break,
284
+ ')' => {
285
+ paren_depth -= 1;
286
+ dest_url.push(')');
287
+ },
288
+ ' ' => break,
289
+ c => dest_url.push(c),
290
+ }
291
+
292
+ k += 1;
293
+ }
294
+
295
+ while chars.get(k) == Some(&' ') {
296
+ k += 1;
297
+ }
298
+
299
+ let mut title = String::new();
300
+ if let Some(quote @ ('"' | '\'')) = chars.get(k).copied() {
301
+ k += 1;
302
+ while k < chars.len() && chars[k] != quote {
303
+ title.push(chars[k]);
304
+ k += 1;
305
+ }
306
+
307
+ if k >= chars.len() {
308
+ return None;
309
+ }
310
+
311
+ k += 1;
312
+ while chars.get(k) == Some(&' ') {
313
+ k += 1;
314
+ }
315
+ }
316
+
317
+ if chars.get(k) != Some(&')') {
318
+ return None;
319
+ }
320
+
321
+ let inner = chars[i + 1..j].iter().collect::<String>();
322
+ let children = resolve(scan(&inner));
323
+ let node = if image {
324
+ Inline::Image { dest_url, children }
325
+ } else {
326
+ Inline::Link {
327
+ dest_url,
328
+ title,
329
+ children,
330
+ }
331
+ };
332
+
333
+ Some((node, k + 1))
334
+ }
335
+
336
+ /// `<https://…>` / `<mailto:…>` autolinks.
337
+ fn parse_autolink(chars: &[char], i: usize) -> Option<(Inline, usize)> {
338
+ let end = i
339
+ + chars[i + 1..]
340
+ .iter()
341
+ .position(|x| *x == '>' || x.is_whitespace() || *x == '<')?
342
+ + 1;
343
+
344
+ if chars[end] != '>' {
345
+ return None;
346
+ }
347
+
348
+ let dest = chars[i + 1..end].iter().collect::<String>();
349
+ let lower = dest.to_ascii_lowercase();
350
+ if !(lower.starts_with("http://")
351
+ || lower.starts_with("https://")
352
+ || lower.starts_with("mailto:"))
353
+ {
354
+ return None;
355
+ }
356
+
357
+ Some((
358
+ Inline::Link {
359
+ dest_url: dest.clone(),
360
+ title: String::new(),
361
+ children: vec![Inline::Text(dest)],
362
+ },
363
+ end + 1,
364
+ ))
365
+ }
366
+
367
+ struct Frame {
368
+ /// `Some((char, run length))` for a pending opener; `None` for the
369
+ /// bottom output frame.
370
+ delim: Option<(char, usize)>,
371
+ children: Vec<Inline>,
372
+ }
373
+
374
+ /// Pair delimiter runs into emphasis nodes: strict nesting against the
375
+ /// frame stack, with non-matching intervening openers demoted to literal
376
+ /// text, and greedy `2 → Strong / 1 → Emphasis` consumption of partial
377
+ /// runs (`***x***` → strong + em).
378
+ fn resolve(items: Vec<ScanItem>) -> Vec<Inline> {
379
+ let mut stack = vec![Frame {
380
+ delim: None,
381
+ children: vec![],
382
+ }];
383
+
384
+ for item in items {
385
+ match item {
386
+ ScanItem::Node(node) => stack.last_mut().unwrap().children.push(node),
387
+ ScanItem::Delim {
388
+ ch,
389
+ len,
390
+ can_open,
391
+ can_close,
392
+ } => {
393
+ let mut remaining = len;
394
+ while remaining > 0 && can_close {
395
+ let Some(fi) = stack.iter().rposition(|x| {
396
+ x.delim
397
+ .is_some_and(|(oc, ol)| oc == ch && (ch != '~' || ol >= 2))
398
+ }) else {
399
+ break;
400
+ };
401
+
402
+ if ch == '~' && remaining < 2 {
403
+ break;
404
+ }
405
+
406
+ demote_above(&mut stack, fi);
407
+ let frame = stack.pop().unwrap();
408
+ let (_, olen) = frame.delim.unwrap();
409
+ let take = if ch != '~' && (olen < 2 || remaining < 2) {
410
+ 1
411
+ } else {
412
+ 2
413
+ };
414
+
415
+ let node = match (ch, take) {
416
+ ('~', _) => Inline::Strikethrough(frame.children),
417
+ (_, 2) => Inline::Strong(frame.children),
418
+ _ => Inline::Emphasis(frame.children),
419
+ };
420
+
421
+ let leftover = olen - take;
422
+ if leftover > 0 {
423
+ stack.push(Frame {
424
+ delim: Some((ch, leftover)),
425
+ children: vec![node],
426
+ });
427
+ } else {
428
+ stack.last_mut().unwrap().children.push(node);
429
+ }
430
+
431
+ remaining -= take;
432
+ }
433
+
434
+ if remaining > 0 {
435
+ if can_open {
436
+ stack.push(Frame {
437
+ delim: Some((ch, remaining)),
438
+ children: vec![],
439
+ });
440
+ } else {
441
+ stack
442
+ .last_mut()
443
+ .unwrap()
444
+ .children
445
+ .push(Inline::Text(std::iter::repeat_n(ch, remaining).collect()));
446
+ }
447
+ }
448
+ },
449
+ }
450
+ }
451
+
452
+ demote_above(&mut stack, 0);
453
+ stack.pop().unwrap().children
454
+ }
455
+
456
+ /// Collapse every frame above `fi` into its parent: the unclosed delimiter
457
+ /// becomes literal text, its children splice through — content is never
458
+ /// dropped.
459
+ fn demote_above(stack: &mut Vec<Frame>, fi: usize) {
460
+ while stack.len() - 1 > fi {
461
+ let frame = stack.pop().unwrap();
462
+ let parent = stack.last_mut().unwrap();
463
+ if let Some((ch, len)) = frame.delim {
464
+ parent
465
+ .children
466
+ .push(Inline::Text(std::iter::repeat_n(ch, len).collect()));
467
+ }
468
+
469
+ parent.children.extend(frame.children);
470
+ }
471
+ }
472
+
473
+ fn emit(nodes: Vec<Inline>, events: &mut Vec<Event>) {
474
+ for node in nodes {
475
+ match node {
476
+ Inline::Text(text) => events.push(Event::Text(text)),
477
+ Inline::Code(text) => events.push(Event::Code(text)),
478
+ Inline::SoftBreak => events.push(Event::SoftBreak),
479
+ Inline::HardBreak => events.push(Event::HardBreak),
480
+ Inline::Emphasis(children) => wrap(Tag::Emphasis, children, events),
481
+ Inline::Strong(children) => wrap(Tag::Strong, children, events),
482
+ Inline::Strikethrough(children) => wrap(Tag::Strikethrough, children, events),
483
+ Inline::Link {
484
+ dest_url,
485
+ title,
486
+ children,
487
+ } => wrap(Tag::Link { dest_url, title }, children, events),
488
+ Inline::Image { dest_url, children } => wrap(Tag::Image { dest_url }, children, events),
489
+ }
490
+ }
491
+ }
492
+
493
+ fn wrap(tag: Tag, children: Vec<Inline>, events: &mut Vec<Event>) {
494
+ events.push(Event::Start(tag));
495
+ emit(children, events);
496
+ events.push(Event::End);
497
+ }