1use crate::PairKind;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41#[non_exhaustive]
42pub enum SlugFamily {
43 PageBreak,
45 Section,
47 BlockContainerOpen,
50 BlockContainerClose,
52 LeafAlign,
55 Bouten,
57 Sashie,
59 Keigakomi,
61 Warichu,
63 TateChuYoko,
65 KaeritenSingle,
68 KaeritenCompound,
70}
71
72#[derive(Debug, Clone, Copy)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub struct SlugEntry {
76 pub canonical: &'static str,
78 pub family: SlugFamily,
80 pub accepts_param: bool,
83 pub doc: &'static str,
87 pub partner: Option<&'static str>,
92 pub wrapper: PairKind,
97}
98
99pub const SLUGS: &[SlugEntry] = &[
105 SlugEntry {
107 canonical: "改ページ",
108 family: SlugFamily::PageBreak,
109 accepts_param: false,
110 doc: "ページを改める",
111 partner: None,
112 wrapper: PairKind::Bracket,
113 },
114 SlugEntry {
115 canonical: "改丁",
116 family: SlugFamily::Section,
117 accepts_param: false,
118 doc: "改丁(次の奇数ページから)",
119 partner: None,
120 wrapper: PairKind::Bracket,
121 },
122 SlugEntry {
123 canonical: "改段",
124 family: SlugFamily::Section,
125 accepts_param: false,
126 doc: "改段(段組を改める)",
127 partner: None,
128 wrapper: PairKind::Bracket,
129 },
130 SlugEntry {
131 canonical: "改見開き",
132 family: SlugFamily::Section,
133 accepts_param: false,
134 doc: "改見開き(次の見開きへ)",
135 partner: None,
136 wrapper: PairKind::Bracket,
137 },
138 SlugEntry {
140 canonical: "ここから字下げ",
141 family: SlugFamily::BlockContainerOpen,
142 accepts_param: false,
143 doc: "1字下げを開始(終わりまで)",
144 partner: Some("ここで字下げ終わり"),
145 wrapper: PairKind::Bracket,
146 },
147 SlugEntry {
148 canonical: "ここから{N}字下げ",
149 family: SlugFamily::BlockContainerOpen,
150 accepts_param: true,
151 doc: "N字下げを開始(終わりまで)",
152 partner: Some("ここで字下げ終わり"),
153 wrapper: PairKind::Bracket,
154 },
155 SlugEntry {
156 canonical: "ここで字下げ終わり",
157 family: SlugFamily::BlockContainerClose,
158 accepts_param: false,
159 doc: "字下げブロックを閉じる",
160 partner: Some("ここから字下げ"),
161 wrapper: PairKind::Bracket,
162 },
163 SlugEntry {
164 canonical: "ここから地付き",
165 family: SlugFamily::BlockContainerOpen,
166 accepts_param: false,
167 doc: "地付きを開始",
168 partner: Some("ここで地付き終わり"),
169 wrapper: PairKind::Bracket,
170 },
171 SlugEntry {
172 canonical: "ここから地から{N}字上げ",
173 family: SlugFamily::BlockContainerOpen,
174 accepts_param: true,
175 doc: "地からN字上げを開始",
176 partner: Some("ここで地付き終わり"),
177 wrapper: PairKind::Bracket,
178 },
179 SlugEntry {
180 canonical: "ここで地付き終わり",
181 family: SlugFamily::BlockContainerClose,
182 accepts_param: false,
183 doc: "地付きブロックを閉じる",
184 partner: Some("ここから地付き"),
185 wrapper: PairKind::Bracket,
186 },
187 SlugEntry {
188 canonical: "罫囲み",
189 family: SlugFamily::Keigakomi,
190 accepts_param: false,
191 doc: "罫線で囲む(終わりまで)",
192 partner: Some("罫囲み終わり"),
193 wrapper: PairKind::Bracket,
194 },
195 SlugEntry {
196 canonical: "罫囲み終わり",
197 family: SlugFamily::Keigakomi,
198 accepts_param: false,
199 doc: "罫囲みを閉じる",
200 partner: Some("罫囲み"),
201 wrapper: PairKind::Bracket,
202 },
203 SlugEntry {
204 canonical: "割り注",
205 family: SlugFamily::Warichu,
206 accepts_param: false,
207 doc: "割り注を開始(終わりまで)",
208 partner: Some("割り注終わり"),
209 wrapper: PairKind::Bracket,
210 },
211 SlugEntry {
212 canonical: "割り注終わり",
213 family: SlugFamily::Warichu,
214 accepts_param: false,
215 doc: "割り注を閉じる",
216 partner: Some("割り注"),
217 wrapper: PairKind::Bracket,
218 },
219 SlugEntry {
221 canonical: "地付き",
222 family: SlugFamily::LeafAlign,
223 accepts_param: false,
224 doc: "前の段落を地付きに揃える",
225 partner: None,
226 wrapper: PairKind::Bracket,
227 },
228 SlugEntry {
229 canonical: "地から{N}字上げ",
230 family: SlugFamily::LeafAlign,
231 accepts_param: true,
232 doc: "前の段落を地からN字上げて揃える",
233 partner: None,
234 wrapper: PairKind::Bracket,
235 },
236 SlugEntry {
237 canonical: "{N}字下げ",
238 family: SlugFamily::LeafAlign,
239 accepts_param: true,
240 doc: "前の段落をN字下げる(単発)",
241 partner: None,
242 wrapper: PairKind::Bracket,
243 },
244 SlugEntry {
246 canonical: "傍点",
247 family: SlugFamily::Bouten,
248 accepts_param: false,
249 doc: "ゴマ傍点([#「対象」に傍点])",
250 partner: None,
251 wrapper: PairKind::Bracket,
252 },
253 SlugEntry {
254 canonical: "白ゴマ傍点",
255 family: SlugFamily::Bouten,
256 accepts_param: false,
257 doc: "白ゴマ傍点",
258 partner: None,
259 wrapper: PairKind::Bracket,
260 },
261 SlugEntry {
262 canonical: "丸傍点",
263 family: SlugFamily::Bouten,
264 accepts_param: false,
265 doc: "丸傍点",
266 partner: None,
267 wrapper: PairKind::Bracket,
268 },
269 SlugEntry {
270 canonical: "白丸傍点",
271 family: SlugFamily::Bouten,
272 accepts_param: false,
273 doc: "白丸傍点",
274 partner: None,
275 wrapper: PairKind::Bracket,
276 },
277 SlugEntry {
278 canonical: "二重丸傍点",
279 family: SlugFamily::Bouten,
280 accepts_param: false,
281 doc: "二重丸傍点",
282 partner: None,
283 wrapper: PairKind::Bracket,
284 },
285 SlugEntry {
286 canonical: "蛇の目傍点",
287 family: SlugFamily::Bouten,
288 accepts_param: false,
289 doc: "蛇の目傍点",
290 partner: None,
291 wrapper: PairKind::Bracket,
292 },
293 SlugEntry {
294 canonical: "ばつ傍点",
295 family: SlugFamily::Bouten,
296 accepts_param: false,
297 doc: "ばつ傍点",
298 partner: None,
299 wrapper: PairKind::Bracket,
300 },
301 SlugEntry {
302 canonical: "白三角傍点",
303 family: SlugFamily::Bouten,
304 accepts_param: false,
305 doc: "白三角傍点",
306 partner: None,
307 wrapper: PairKind::Bracket,
308 },
309 SlugEntry {
310 canonical: "波線",
311 family: SlugFamily::Bouten,
312 accepts_param: false,
313 doc: "波線(傍線の波形)",
314 partner: None,
315 wrapper: PairKind::Bracket,
316 },
317 SlugEntry {
318 canonical: "傍線",
319 family: SlugFamily::Bouten,
320 accepts_param: false,
321 doc: "傍線(下線)",
322 partner: None,
323 wrapper: PairKind::Bracket,
324 },
325 SlugEntry {
326 canonical: "二重傍線",
327 family: SlugFamily::Bouten,
328 accepts_param: false,
329 doc: "二重傍線(二重下線)",
330 partner: None,
331 wrapper: PairKind::Bracket,
332 },
333 SlugEntry {
335 canonical: "挿絵({path})入る",
336 family: SlugFamily::Sashie,
337 accepts_param: true,
338 doc: "挿絵を埋め込む",
339 partner: None,
340 wrapper: PairKind::Bracket,
341 },
342 SlugEntry {
343 canonical: "縦中横",
344 family: SlugFamily::TateChuYoko,
345 accepts_param: false,
346 doc: "縦中横([#「対象」は縦中横])",
347 partner: None,
348 wrapper: PairKind::Bracket,
349 },
350 SlugEntry {
352 canonical: "一",
353 family: SlugFamily::KaeritenSingle,
354 accepts_param: false,
355 doc: "返り点 一",
356 partner: None,
357 wrapper: PairKind::Bracket,
358 },
359 SlugEntry {
360 canonical: "二",
361 family: SlugFamily::KaeritenSingle,
362 accepts_param: false,
363 doc: "返り点 二",
364 partner: None,
365 wrapper: PairKind::Bracket,
366 },
367 SlugEntry {
368 canonical: "三",
369 family: SlugFamily::KaeritenSingle,
370 accepts_param: false,
371 doc: "返り点 三",
372 partner: None,
373 wrapper: PairKind::Bracket,
374 },
375 SlugEntry {
376 canonical: "四",
377 family: SlugFamily::KaeritenSingle,
378 accepts_param: false,
379 doc: "返り点 四",
380 partner: None,
381 wrapper: PairKind::Bracket,
382 },
383 SlugEntry {
384 canonical: "上",
385 family: SlugFamily::KaeritenSingle,
386 accepts_param: false,
387 doc: "返り点 上",
388 partner: None,
389 wrapper: PairKind::Bracket,
390 },
391 SlugEntry {
392 canonical: "中",
393 family: SlugFamily::KaeritenSingle,
394 accepts_param: false,
395 doc: "返り点 中",
396 partner: None,
397 wrapper: PairKind::Bracket,
398 },
399 SlugEntry {
400 canonical: "下",
401 family: SlugFamily::KaeritenSingle,
402 accepts_param: false,
403 doc: "返り点 下",
404 partner: None,
405 wrapper: PairKind::Bracket,
406 },
407 SlugEntry {
408 canonical: "甲",
409 family: SlugFamily::KaeritenSingle,
410 accepts_param: false,
411 doc: "返り点 甲",
412 partner: None,
413 wrapper: PairKind::Bracket,
414 },
415 SlugEntry {
416 canonical: "乙",
417 family: SlugFamily::KaeritenSingle,
418 accepts_param: false,
419 doc: "返り点 乙",
420 partner: None,
421 wrapper: PairKind::Bracket,
422 },
423 SlugEntry {
424 canonical: "丙",
425 family: SlugFamily::KaeritenSingle,
426 accepts_param: false,
427 doc: "返り点 丙",
428 partner: None,
429 wrapper: PairKind::Bracket,
430 },
431 SlugEntry {
432 canonical: "丁",
433 family: SlugFamily::KaeritenSingle,
434 accepts_param: false,
435 doc: "返り点 丁",
436 partner: None,
437 wrapper: PairKind::Bracket,
438 },
439 SlugEntry {
440 canonical: "レ",
441 family: SlugFamily::KaeritenSingle,
442 accepts_param: false,
443 doc: "返り点 レ",
444 partner: None,
445 wrapper: PairKind::Bracket,
446 },
447 SlugEntry {
449 canonical: "一レ",
450 family: SlugFamily::KaeritenCompound,
451 accepts_param: false,
452 doc: "返り点 一レ",
453 partner: None,
454 wrapper: PairKind::Bracket,
455 },
456 SlugEntry {
457 canonical: "二レ",
458 family: SlugFamily::KaeritenCompound,
459 accepts_param: false,
460 doc: "返り点 二レ",
461 partner: None,
462 wrapper: PairKind::Bracket,
463 },
464 SlugEntry {
465 canonical: "三レ",
466 family: SlugFamily::KaeritenCompound,
467 accepts_param: false,
468 doc: "返り点 三レ",
469 partner: None,
470 wrapper: PairKind::Bracket,
471 },
472 SlugEntry {
473 canonical: "上レ",
474 family: SlugFamily::KaeritenCompound,
475 accepts_param: false,
476 doc: "返り点 上レ",
477 partner: None,
478 wrapper: PairKind::Bracket,
479 },
480 SlugEntry {
481 canonical: "中レ",
482 family: SlugFamily::KaeritenCompound,
483 accepts_param: false,
484 doc: "返り点 中レ",
485 partner: None,
486 wrapper: PairKind::Bracket,
487 },
488 SlugEntry {
489 canonical: "下レ",
490 family: SlugFamily::KaeritenCompound,
491 accepts_param: false,
492 doc: "返り点 下レ",
493 partner: None,
494 wrapper: PairKind::Bracket,
495 },
496];
497
498const VARIANTS: &[(&str, &str)] = &[
504 ("ぼうてん", "傍点"),
506 ("にぼうてん", "傍点"),
507 ("しろぼうてん", "白ゴマ傍点"),
508 ("しろごまぼうてん", "白ゴマ傍点"),
509 ("まるぼうてん", "丸傍点"),
510 ("にまるぼうてん", "丸傍点"),
511 ("しろまるぼうてん", "白丸傍点"),
512 ("にしろまるぼうてん", "白丸傍点"),
513 ("にじゅうまるぼうてん", "二重丸傍点"),
514 ("じゃのめぼうてん", "蛇の目傍点"),
515 ("ばつぼうてん", "ばつ傍点"),
516 ("しろさんかくぼうてん", "白三角傍点"),
517 ("はせん", "波線"),
518 ("ぼうせん", "傍線"),
519 ("にじゅうぼうせん", "二重傍線"),
520 ("かいぺーじ", "改ページ"),
522 ("ページかえ", "改ページ"),
523 ("かいちょう", "改丁"),
524 ("かいだん", "改段"),
525 ("かいみひらき", "改見開き"),
526 ("ここからじさげ", "ここから字下げ"),
528 ("ここでじさげおわり", "ここで字下げ終わり"),
529 ("ここからじつき", "ここから地付き"),
530 ("ここでじつきおわり", "ここで地付き終わり"),
531 ("じつき", "地付き"),
533 ("たてちゅうよこ", "縦中横"),
535 ("たて中横", "縦中横"),
536 ("そうにゅうえ", "挿絵({path})入る"),
537 ("けいがこみ", "罫囲み"),
539 ("けいがこみおわり", "罫囲み終わり"),
540 ("わりちゅう", "割り注"),
541 ("わりちゅうおわり", "割り注終わり"),
542];
543
544#[must_use]
559pub fn canonicalise_slug(input: &str) -> Option<&'static str> {
560 for entry in SLUGS {
563 if entry.canonical == input {
564 return Some(entry.canonical);
565 }
566 }
567 for &(variant, canonical) in VARIANTS {
568 if variant == input {
569 return Some(canonical);
570 }
571 }
572 None
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578
579 #[test]
580 fn slugs_table_is_non_empty() {
581 assert!(!SLUGS.is_empty());
582 }
583
584 #[test]
585 fn slugs_have_unique_canonical_strings() {
586 let mut seen: Vec<&'static str> = Vec::with_capacity(SLUGS.len());
587 for entry in SLUGS {
588 assert!(
589 !seen.contains(&entry.canonical),
590 "duplicate canonical: {}",
591 entry.canonical
592 );
593 seen.push(entry.canonical);
594 }
595 }
596
597 #[test]
598 fn every_canonical_is_self_canonical() {
599 for entry in SLUGS {
600 let resolved = canonicalise_slug(entry.canonical)
601 .unwrap_or_else(|| panic!("canonical {} did not resolve", entry.canonical));
602 assert_eq!(resolved, entry.canonical);
603 }
604 }
605
606 #[test]
607 fn known_hiragana_variants_resolve_to_canonical() {
608 assert_eq!(canonicalise_slug("ぼうてん"), Some("傍点"));
609 assert_eq!(canonicalise_slug("にぼうてん"), Some("傍点"));
610 assert_eq!(canonicalise_slug("しろまるぼうてん"), Some("白丸傍点"));
611 assert_eq!(canonicalise_slug("ここからじさげ"), Some("ここから字下げ"));
612 }
613
614 #[test]
615 fn unknown_input_returns_none() {
616 assert_eq!(canonicalise_slug("nonsense"), None);
617 assert_eq!(canonicalise_slug(""), None);
618 }
619
620 #[test]
621 fn paired_slugs_reference_existing_partner() {
622 for entry in SLUGS {
623 if let Some(partner) = entry.partner {
624 let found = SLUGS.iter().any(|e| e.canonical == partner);
625 assert!(
626 found,
627 "partner {partner} not in SLUGS for {}",
628 entry.canonical
629 );
630 }
631 }
632 }
633
634 #[test]
635 fn block_container_open_pairs_with_close() {
636 for entry in SLUGS {
639 match entry.family {
640 SlugFamily::BlockContainerOpen => {
641 let partner_canonical = entry
642 .partner
643 .unwrap_or_else(|| panic!("open {} has no partner", entry.canonical));
644 let partner = SLUGS
645 .iter()
646 .find(|e| e.canonical == partner_canonical)
647 .expect("partner exists");
648 assert!(matches!(
649 partner.family,
650 SlugFamily::BlockContainerClose
651 | SlugFamily::Keigakomi
652 | SlugFamily::Warichu
653 ));
654 }
655 SlugFamily::BlockContainerClose => {
656 let partner_canonical = entry
657 .partner
658 .unwrap_or_else(|| panic!("close {} has no partner", entry.canonical));
659 let partner = SLUGS
660 .iter()
661 .find(|e| e.canonical == partner_canonical)
662 .expect("partner exists");
663 assert!(matches!(
664 partner.family,
665 SlugFamily::BlockContainerOpen
666 | SlugFamily::Keigakomi
667 | SlugFamily::Warichu
668 ));
669 }
670 _ => {}
671 }
672 }
673 }
674
675 #[test]
676 fn accepts_param_aligns_with_brace_in_canonical() {
677 for entry in SLUGS {
680 let has_brace = entry.canonical.contains('{');
681 assert_eq!(
682 entry.accepts_param, has_brace,
683 "accepts_param/brace mismatch on {}",
684 entry.canonical
685 );
686 }
687 }
688
689 #[test]
690 fn variant_table_resolves_to_strings_in_slugs() {
691 for &(variant, canonical) in VARIANTS {
692 assert!(
693 SLUGS.iter().any(|e| e.canonical == canonical),
694 "variant {variant} maps to unknown canonical {canonical}"
695 );
696 }
697 }
698}