@perspective-dev/viewer 5.0.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 (213) 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/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/docs/perspective-docs.json +1 -0
  24. package/dist/esm/column-format.d.ts +19 -28
  25. package/dist/esm/perspective-viewer.d.ts +2 -0
  26. package/dist/esm/perspective-viewer.inline.js +8 -8
  27. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  28. package/dist/esm/perspective-viewer.js +9 -9
  29. package/dist/esm/perspective-viewer.js.map +4 -4
  30. package/dist/esm/providers.d.ts +79 -0
  31. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  32. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  33. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  34. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  35. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  36. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  37. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  38. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  39. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  40. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  41. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  42. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  43. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  44. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  45. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +43 -1
  46. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  47. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  48. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  49. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  50. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  51. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  52. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  53. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  54. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  55. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  56. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  57. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  58. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  59. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  60. package/dist/esm/ts-rs/ViewerConfig.d.ts +43 -1
  61. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  62. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +72 -0
  63. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  64. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  65. package/dist/esm/ts-rs/WindowSpec.d.ts +47 -0
  66. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  67. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
  68. package/dist/wasm/perspective-viewer.d.ts +222 -26
  69. package/dist/wasm/perspective-viewer.js +378 -125
  70. package/dist/wasm/perspective-viewer.wasm +0 -0
  71. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
  72. package/package.json +2 -1
  73. package/src/css/_viewer_bundle.css +1 -0
  74. package/src/css/column-selector.css +9 -0
  75. package/src/css/column-settings-panel.css +67 -2
  76. package/src/css/column-style.css +4 -3
  77. package/src/css/config-selector.css +60 -4
  78. package/src/css/containers/context-menu.css +12 -6
  79. package/src/css/containers/tabs.css +2 -2
  80. package/src/css/form/code-editor.css +2 -3
  81. package/src/css/form/mirrored-textarea.css +81 -0
  82. package/src/css/viewer.css +336 -0
  83. package/src/rust/agent/client/mod.rs +19 -0
  84. package/src/rust/agent/client/protocol.rs +320 -0
  85. package/src/rust/agent/client/transport.rs +287 -0
  86. package/src/rust/agent/client/turn.rs +174 -0
  87. package/src/rust/agent/config.rs +159 -0
  88. package/src/rust/agent/docs.rs +293 -0
  89. package/src/rust/agent/markdown/block.rs +509 -0
  90. package/src/rust/agent/markdown/entity.rs +52 -0
  91. package/src/rust/agent/markdown/events.rs +71 -0
  92. package/src/rust/agent/markdown/inline.rs +497 -0
  93. package/src/rust/agent/markdown/mod.rs +466 -0
  94. package/src/rust/agent/mod.rs +23 -0
  95. package/src/rust/agent/preamble.md +16 -0
  96. package/src/rust/agent/runtime.rs +201 -0
  97. package/src/rust/agent/slot.rs +236 -0
  98. package/src/rust/agent/tools.rs +1110 -0
  99. package/src/rust/components/chat_panel.rs +289 -0
  100. package/src/rust/components/column_selector/active_column.rs +21 -27
  101. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  102. package/src/rust/components/column_selector/config_selector.rs +83 -8
  103. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  104. package/src/rust/components/column_selector/inactive_column.rs +8 -1
  105. package/src/rust/components/column_selector.rs +27 -3
  106. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  107. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  108. package/src/rust/components/column_settings_sidebar.rs +163 -16
  109. package/src/rust/components/containers/dragdrop_list.rs +110 -3
  110. package/src/rust/components/containers/sidebar.rs +99 -22
  111. package/src/rust/components/containers/split_panel.rs +6 -0
  112. package/src/rust/components/editable_header.rs +2 -0
  113. package/src/rust/components/form/code_editor.rs +14 -23
  114. package/src/rust/components/form/debug.rs +8 -5
  115. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  116. package/src/rust/components/form/mod.rs +1 -0
  117. package/src/rust/components/main_panel.rs +5 -0
  118. package/src/rust/components/mod.rs +3 -0
  119. package/src/rust/components/panel_menu.rs +7 -0
  120. package/src/rust/components/panel_tab.rs +5 -0
  121. package/src/rust/components/portal.rs +7 -0
  122. package/src/rust/components/settings_panel.rs +80 -2
  123. package/src/rust/components/viewer/msg.rs +2 -0
  124. package/src/rust/components/viewer/panels.rs +9 -14
  125. package/src/rust/components/viewer/render.rs +14 -0
  126. package/src/rust/components/viewer/settings.rs +33 -1
  127. package/src/rust/components/viewer.rs +2 -0
  128. package/src/rust/components/window_editor.rs +985 -0
  129. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  130. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  131. package/src/rust/config/number_string_format.rs +33 -0
  132. package/src/rust/config/options.rs +18 -0
  133. package/src/rust/config/plugin_static_config.rs +120 -5
  134. package/src/rust/config/viewer_config.rs +220 -1
  135. package/src/rust/config/workspace_config.rs +7 -6
  136. package/src/rust/custom_elements/viewer.rs +209 -18
  137. package/src/rust/custom_events.rs +29 -0
  138. package/src/rust/lib.rs +17 -1
  139. package/src/rust/presentation/column_locator.rs +16 -1
  140. package/src/rust/presentation.rs +8 -0
  141. package/src/rust/queries/columns_iter_set.rs +7 -1
  142. package/src/rust/queries/get_viewer_config.rs +5 -1
  143. package/src/rust/renderer/dispatch.rs +85 -58
  144. package/src/rust/renderer.rs +34 -1
  145. package/src/rust/session/column_defaults_update.rs +18 -0
  146. package/src/rust/session/drag_drop_update.rs +18 -0
  147. package/src/rust/session/metadata.rs +64 -9
  148. package/src/rust/session/replace_expression_update.rs +2 -0
  149. package/src/rust/session.rs +7 -5
  150. package/src/rust/tasks/create_panel.rs +18 -8
  151. package/src/rust/tasks/edit_window.rs +155 -0
  152. package/src/rust/tasks/mod.rs +2 -0
  153. package/src/rust/tasks/restore_panel.rs +34 -17
  154. package/src/rust/tasks/sync_update_panels.rs +7 -23
  155. package/src/rust/utils/browser/dragdrop.rs +12 -0
  156. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  157. package/src/rust/utils/debounce.rs +76 -27
  158. package/src/rust/workspace.rs +112 -20
  159. package/src/svg/pin-icon.svg +5 -0
  160. package/src/themes/icons.css +1 -0
  161. package/src/themes/intl/de.css +23 -0
  162. package/src/themes/intl/es.css +23 -0
  163. package/src/themes/intl/fr.css +23 -0
  164. package/src/themes/intl/ja.css +23 -0
  165. package/src/themes/intl/pt.css +23 -0
  166. package/src/themes/intl/zh.css +23 -0
  167. package/src/themes/intl.css +26 -1
  168. package/src/ts/column-format.ts +67 -46
  169. package/src/ts/perspective-viewer.ts +2 -0
  170. package/src/ts/providers.ts +96 -0
  171. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  172. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  173. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  174. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  175. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  176. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  177. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  178. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  179. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  180. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  181. package/src/ts/ts-rs/FormatMode.ts +3 -0
  182. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  183. package/src/ts/ts-rs/Notation.ts +11 -0
  184. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  185. package/src/ts/ts-rs/PanelViewerConfig.ts +48 -1
  186. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  187. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  188. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  189. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  190. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  191. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  192. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  193. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  194. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  195. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  196. package/src/ts/ts-rs/Unit.ts +3 -0
  197. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  198. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  199. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  200. package/src/ts/ts-rs/ViewerConfig.ts +48 -1
  201. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  202. package/src/ts/ts-rs/ViewerConfigUpdate.ts +81 -2
  203. package/src/ts/ts-rs/WindowSort.ts +9 -0
  204. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  205. package/src/ts/ts-rs/WindowSpec.ts +47 -0
  206. package/src/ts/ts-rs/Windows.ts +10 -0
  207. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
  208. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
  209. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
  210. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
  211. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
  212. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
  213. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → 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
+ }