1use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4use unicode_segmentation::UnicodeSegmentation;
5
6#[derive(Debug, Clone, Deserialize, Serialize)]
7#[serde(rename_all = "camelCase")]
8pub struct WarichuOptions {
9 pub first_capacity: usize,
10 #[serde(alias = "capacity")]
11 pub continuation_capacity: usize,
12}
13
14#[derive(Debug, Clone, Serialize)]
15#[serde(rename_all = "camelCase")]
16pub struct WarichuSource {
17 pub path: Vec<usize>,
18 pub start_utf8: usize,
19 pub end_utf8: usize,
20 pub group: usize,
21}
22
23#[derive(Debug, Serialize)]
24#[serde(rename_all = "camelCase")]
25pub struct WarichuFragment {
26 pub lines: [Vec<Value>; 2],
27 pub sources: [Vec<WarichuSource>; 2],
28 pub html: [String; 2],
29 pub widths: [usize; 2],
31 pub overflow: bool,
32 pub hard_break_after: bool,
33}
34
35struct Unit {
36 nodes: Vec<Value>,
37 sources: Vec<WarichuSource>,
38 is_text: bool,
39 text: String,
40 width: usize,
41 hard_break: bool,
42}
43
44fn visible(node: &Value) -> String {
45 match node["type"].as_str().unwrap_or_default() {
46 "comment" | "mdiComment" => String::new(),
47 "ruby" => node["base"].as_str().unwrap_or_default().to_owned(),
48 "image" => node["alt"].as_str().unwrap_or_default().to_owned(),
49 _ => node["value"]
50 .as_str()
51 .map(str::to_owned)
52 .unwrap_or_else(|| crate::children(node).iter().map(visible).collect()),
53 }
54}
55
56fn weight(text: &str) -> usize {
57 text.graphemes(true)
58 .map(|g| {
59 if g.chars()
60 .all(|c| c.is_ascii() || ('\u{ff61}'..='\u{ff9f}').contains(&c))
61 {
62 1
63 } else {
64 2
65 }
66 })
67 .sum()
68}
69
70fn without_comments(mut node: Value) -> Value {
71 if let Some(children) = node.get_mut("children").and_then(Value::as_array_mut) {
72 *children = std::mem::take(children)
73 .into_iter()
74 .filter(|child| !matches!(child["type"].as_str(), Some("comment" | "mdiComment")))
75 .map(without_comments)
76 .collect();
77 }
78 node
79}
80
81fn units(nodes: &[Value], wrappers: &[Value], path: &[usize], out: &mut Vec<Unit>) {
82 for (index, node) in nodes.iter().enumerate() {
83 let mut path = path.to_vec();
84 path.push(index);
85 let kind = node["type"].as_str().unwrap_or_default();
86 if matches!(kind, "comment" | "mdiComment") {
87 continue;
88 }
89 if matches!(
90 kind,
91 "strong" | "emphasis" | "delete" | "em" | "kern" | "link"
92 ) {
93 let mut parents = wrappers.to_vec();
94 parents.push(node.clone());
95 units(crate::children(node), &parents, &path, out);
96 continue;
97 }
98 let parts = if kind == "text" {
99 node["value"]
100 .as_str()
101 .unwrap_or_default()
102 .chars()
103 .map(|g| json!({"type":"text", "value":g.to_string()}))
104 .collect::<Vec<_>>()
105 } else {
106 vec![without_comments(node.clone())]
107 };
108 let mut offset = 0;
109 for mut part in parts {
110 let text = visible(&part);
111 let width = weight(&text);
112 for wrapper in wrappers.iter().rev() {
113 let mut parent = wrapper.clone();
114 parent["children"] = json!([part]);
115 part = parent;
116 }
117 out.push(Unit {
118 nodes: vec![part],
119 sources: vec![WarichuSource {
120 path: path.clone(),
121 start_utf8: offset,
122 end_utf8: offset + text.len(),
123 group: 0,
124 }],
125 is_text: kind == "text",
126 text: text.clone(),
127 width,
128 hard_break: kind == "break",
129 });
130 offset += text.len();
131 }
132 }
133}
134
135fn join_graphemes(input: Vec<Unit>) -> Vec<Unit> {
137 let mut result: Vec<Unit> = Vec::new();
138 let mut input = input.into_iter().peekable();
139 while let Some(unit) = input.next() {
140 if !unit.is_text {
141 result.push(unit);
142 continue;
143 }
144 let mut run = vec![unit];
145 while input.peek().is_some_and(|unit| unit.is_text) {
146 run.push(input.next().unwrap());
147 }
148 let text: String = run.iter().map(|unit| unit.text.as_str()).collect();
149 let mut boundaries = text
150 .grapheme_indices(true)
151 .map(|(offset, _)| offset)
152 .peekable();
153 let mut offset = 0;
154 for unit in run {
155 if boundaries.peek() == Some(&offset) {
156 boundaries.next();
157 result.push(unit);
158 } else {
159 let previous = result.last_mut().unwrap();
160 previous.text.push_str(&unit.text);
161 previous.nodes.extend(unit.nodes);
162 previous.sources.extend(unit.sources);
163 }
164 offset += result
165 .last()
166 .unwrap()
167 .text
168 .chars()
169 .last()
170 .unwrap()
171 .len_utf8();
172 }
173 }
174 for (group, unit) in result.iter_mut().enumerate() {
175 unit.width = weight(&unit.text);
176 for source in &mut unit.sources {
177 source.group = group;
178 }
179 }
180 result
181}
182
183fn legal(left: &Unit, right: &Unit) -> bool {
185 let opening = "(〔[{〈《「『【([{‘“";
186 let closing = "、。,.・:;?!ー々ゝゞヽヾ)〕]}〉》」』】)]},.!?:;’”ぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮヵヶ";
187 !left
188 .text
189 .chars()
190 .last()
191 .is_some_and(|c| opening.contains(c))
192 && !right
193 .text
194 .chars()
195 .next()
196 .is_some_and(|c| closing.contains(c))
197}
198
199fn emit_run(run: &[Unit], options: &WarichuOptions, out: &mut Vec<WarichuFragment>) {
200 let mut start = 0;
201 while start < run.len() {
202 let capacity = if out.is_empty() {
203 options.first_capacity
204 } else {
205 options.continuation_capacity
206 }
207 .max(1);
208 let mut end = start;
209 let mut total = 0;
210 while end < run.len()
211 && (total + run[end].width <= capacity.saturating_mul(2) || end == start)
212 {
213 total += run[end].width;
214 end += 1;
215 }
216 let target = end;
219 while end > start && end < run.len() && !legal(&run[end - 1], &run[end]) {
220 end -= 1;
221 }
222 if end == start {
223 end = target;
224 while end < run.len() && !legal(&run[end - 1], &run[end]) {
225 end += 1;
226 }
227 }
228 total = run[start..end].iter().map(|u| u.width).sum();
229 let mut first = 0;
230 let mut best = None;
231 for split in start + 1..end {
232 first += run[split - 1].width;
233 if !legal(&run[split - 1], &run[split]) {
234 continue;
235 }
236 let second = total - first;
237 let score = (
238 first.max(second) > capacity,
239 first.abs_diff(second),
240 first < second,
241 );
242 if best.as_ref().is_none_or(|(old, _, _)| score < *old) {
243 best = Some((score, split, first));
244 }
245 }
246 let (_, split, first) = best.unwrap_or(((true, total, false), end, total));
247 let second = total - first;
248 out.push(WarichuFragment {
249 lines: [
250 run[start..split]
251 .iter()
252 .flat_map(|u| u.nodes.clone())
253 .collect(),
254 run[split..end]
255 .iter()
256 .flat_map(|u| u.nodes.clone())
257 .collect(),
258 ],
259 sources: [
260 run[start..split]
261 .iter()
262 .flat_map(|u| u.sources.clone())
263 .collect(),
264 run[split..end]
265 .iter()
266 .flat_map(|u| u.sources.clone())
267 .collect(),
268 ],
269 html: [
270 render_units(&run[start..split]),
271 render_units(&run[split..end]),
272 ],
273 widths: [first, second],
274 overflow: first.max(second) > capacity,
275 hard_break_after: false,
276 });
277 start = end;
278 }
279}
280
281pub fn layout_warichu(nodes: &[Value], capacity: usize) -> Vec<WarichuFragment> {
285 layout_warichu_with_options(
286 nodes,
287 &WarichuOptions {
288 first_capacity: capacity,
289 continuation_capacity: capacity,
290 },
291 )
292}
293
294pub fn layout_warichu_with_options(
295 nodes: &[Value],
296 options: &WarichuOptions,
297) -> Vec<WarichuFragment> {
298 let mut input = Vec::new();
299 units(nodes, &[], &[], &mut input);
300 let input = join_graphemes(input);
301 let mut out = Vec::new();
302 let mut start = 0;
303 for (index, unit) in input.iter().enumerate() {
304 if unit.hard_break {
305 let before = out.len();
306 emit_run(&input[start..index], options, &mut out);
307 if out.len() == before {
308 out.push(WarichuFragment {
309 lines: [vec![], vec![]],
310 sources: [vec![], vec![]],
311 html: [String::new(), String::new()],
312 widths: [0, 0],
313 overflow: false,
314 hard_break_after: true,
315 });
316 } else {
317 out.last_mut().unwrap().hard_break_after = true;
318 }
319 start = index + 1;
320 }
321 }
322 emit_run(&input[start..], options, &mut out);
323 out
324}
325
326fn render_units(units: &[Unit]) -> String {
327 let mut out = String::new();
328 for unit in units {
329 for node in &unit.nodes {
330 crate::render_html_node(node, &mut out);
331 }
332 }
333 out.replace(
336 "<span class=\"mdi-warichu\" style=\"font-size:.5em;line-height:1\"",
337 "<span class=\"mdi-warichu\" style=\"font-size:1em;line-height:1\"",
338 )
339}
340
341pub fn layout_warichu_options_json(nodes: &str, options: &str) -> Result<String, String> {
342 let nodes: Vec<Value> = serde_json::from_str(nodes).map_err(|e| e.to_string())?;
343 let options: WarichuOptions = serde_json::from_str(options).map_err(|e| e.to_string())?;
344 serde_json::to_string(&layout_warichu_with_options(&nodes, &options)).map_err(|e| e.to_string())
345}
346
347pub(crate) fn render(nodes: &[Value], out: &mut String) {
348 out.push_str("<span class=\"mdi-warichu\" style=\"font-size:.5em;line-height:1\" data-mdi-warichu-source=\"");
349 let source = serde_json::to_string(nodes).unwrap();
350 out.push_str(
351 &source
352 .replace('&', "&")
353 .replace('"', """)
354 .replace('<', "<")
355 .replace('>', ">"),
356 );
357 out.push_str("\">");
358 for fragment in layout_warichu(nodes, 40) {
359 out.push_str("<span class=\"mdi-warichu-fragment\" style=\"display:inline-flex;flex-direction:column;vertical-align:middle;text-align:start\"");
360 if fragment.overflow {
361 out.push_str(" data-mdi-overflow=\"indivisible\"");
362 }
363 out.push('>');
364 for line in fragment.html {
365 out.push_str("<span class=\"mdi-warichu-line\" style=\"display:block;white-space:nowrap;min-block-size:1em\">");
366 out.push_str(&line);
367 out.push_str("</span>");
368 }
369 out.push_str("</span>");
370 if fragment.hard_break_after {
371 out.push_str("<br>");
372 }
373 }
374 out.push_str("</span>");
375}