1use std::ops::Range;
24use std::sync::Arc;
25
26use rich::measure::Measurement;
27use rich::text::Span;
28use rich::{Console, ConsoleOptions, Renderable, Segment, Text};
29use rich_plugin_api::{PluginError, TextTransform};
30
31use crate::model::MicroAsset;
32use crate::name::is_valid_name;
33use crate::registry::MicroRegistry;
34use crate::render::{
35 FallbackPreference, MicroMeta, PAD_CELL, SENTINEL_BASE, SENTINEL_LAST, fallback_cells,
36 is_sentinel, placeholder_with,
37};
38
39pub const TOKEN_PREFIX: &str = ":micro:";
41
42#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct Diagnostic {
45 pub offset: usize,
48 pub token: String,
50 pub kind: DiagnosticKind,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub enum DiagnosticKind {
55 UnknownAsset(String),
57 Malformed,
59 Reserved,
63}
64
65impl std::fmt::Display for Diagnostic {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match &self.kind {
68 DiagnosticKind::UnknownAsset(name) => {
69 write!(f, "unknown micro asset {name:?} at byte {}", self.offset)
70 }
71 DiagnosticKind::Malformed => write!(
72 f,
73 "{:?} at byte {} is not a micro asset token (write :micro:name:)",
74 self.token, self.offset
75 ),
76 DiagnosticKind::Reserved => write!(
77 f,
78 "{:?} at byte {} was left as written: the input holds reserved \
79 private-use characters",
80 self.token, self.offset
81 ),
82 }
83 }
84}
85
86enum Token {
87 Escape(usize),
89 Asset {
90 range: Range<usize>,
91 name: String,
92 },
93 Malformed(Range<usize>),
94}
95
96fn scan(s: &str, skip_tags: bool) -> Vec<Token> {
99 let bytes = s.as_bytes();
100 let mut tokens = Vec::new();
101 let mut at = 0;
102 while at < bytes.len() {
103 if skip_tags && bytes[at] == b'[' {
104 let backslashes = bytes[..at]
105 .iter()
106 .rev()
107 .take_while(|b| **b == b'\\')
108 .count();
109 let opens_tag = bytes
110 .get(at + 1)
111 .is_some_and(|b| b.is_ascii_lowercase() || matches!(b, b'#' | b'/' | b'@'));
112 if backslashes % 2 == 0 && opens_tag {
113 if let Some(close) = s[at + 1..].find([']', '['])
115 && bytes[at + 1 + close] == b']'
116 {
117 at += close + 2;
118 continue;
119 }
120 }
121 }
122 if s[at..].starts_with(TOKEN_PREFIX) {
123 if at > 0 && bytes[at - 1] == b'\\' {
124 tokens.push(Token::Escape(at - 1));
125 at += TOKEN_PREFIX.len();
126 continue;
127 }
128 let body = at + TOKEN_PREFIX.len();
129 let length = bytes[body..]
130 .iter()
131 .take_while(|b| {
132 b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'-' | b'/')
133 })
134 .count();
135 let name = &s[body..body + length];
136 if bytes.get(body + length) == Some(&b':') && is_valid_name(name) {
137 tokens.push(Token::Asset {
138 range: at..body + length + 1,
139 name: name.to_string(),
140 });
141 at = body + length + 1;
142 } else {
143 tokens.push(Token::Malformed(at..body));
144 at = body;
145 }
146 continue;
147 }
148 at += s[at..].chars().next().map_or(1, char::len_utf8);
149 }
150 tokens
151}
152
153struct Edit {
155 range: Range<usize>,
156 insert: Option<Text>,
157}
158
159fn splice(text: &Text, edits: &[Edit]) -> Text {
164 if edits.is_empty() {
165 return text.clone();
166 }
167 let plain = text.plain();
168 let mut out = String::with_capacity(plain.len());
169 let mut moved: Vec<(Range<usize>, Range<usize>)> = Vec::with_capacity(edits.len());
171 let mut inserted: Vec<Span> = Vec::new();
172 let mut cursor = 0;
173 for edit in edits {
174 out.push_str(&plain[cursor..edit.range.start]);
175 let start = out.len();
176 if let Some(insert) = &edit.insert {
177 out.push_str(insert.plain());
178 for span in insert.spans() {
179 inserted.push(Span {
180 start: span.start + start,
181 end: span.end + start,
182 style: span.style.clone(),
183 });
184 }
185 }
186 moved.push((edit.range.clone(), start..out.len()));
187 cursor = edit.range.end;
188 }
189 out.push_str(&plain[cursor..]);
190 let map = |position: usize, is_end: bool| -> usize {
191 let mut delta: isize = 0;
192 for (old, new) in &moved {
193 if position <= old.start {
194 break;
195 }
196 if position < old.end {
197 return if is_end { new.end } else { new.start };
198 }
199 delta = new.end as isize - old.end as isize;
200 }
201 (position as isize + delta) as usize
202 };
203 let mut result = text.blank_copy();
204 result.append(&out, None);
205 let mut spans: Vec<Span> = text
206 .spans()
207 .iter()
208 .filter_map(|span| {
209 let start = map(span.start, false);
210 let end = map(span.end, true);
211 (end > start).then(|| Span {
212 start,
213 end,
214 style: span.style.clone(),
215 })
216 })
217 .collect();
218 spans.extend(inserted);
219 result.set_spans(spans);
220 result
221}
222
223pub fn expand(
227 text: &Text,
228 registry: &MicroRegistry,
229 preference: FallbackPreference,
230) -> (Text, Vec<Diagnostic>) {
231 let plain = text.plain();
232 let mut edits = Vec::new();
233 let mut diagnostics = Vec::new();
234 for token in scan(plain, false) {
235 match token {
236 Token::Escape(at) => edits.push(Edit {
237 range: at..at + 1,
238 insert: None,
239 }),
240 Token::Asset { range, name } => match registry.resolve(&name) {
241 Some(asset) => edits.push(Edit {
242 range,
243 insert: Some(placeholder_with(asset, preference, MicroMeta::new(asset))),
244 }),
245 None => diagnostics.push(Diagnostic {
246 offset: range.start,
247 token: plain[range].to_string(),
248 kind: DiagnosticKind::UnknownAsset(name),
249 }),
250 },
251 Token::Malformed(range) => diagnostics.push(Diagnostic {
252 offset: range.start,
253 token: plain[range].to_string(),
254 kind: DiagnosticKind::Malformed,
255 }),
256 }
257 }
258 (splice(text, &edits), diagnostics)
259}
260
261#[derive(Clone, Debug)]
276pub struct PreparedMarkup {
277 markup: String,
278 tokens: Vec<(char, Arc<MicroAsset>)>,
280 preference: FallbackPreference,
281 diagnostics: Vec<Diagnostic>,
282}
283
284impl PreparedMarkup {
285 pub fn new(markup: &str, registry: &MicroRegistry, preference: FallbackPreference) -> Self {
286 let mut prepared = PreparedMarkup {
287 markup: String::with_capacity(markup.len()),
288 tokens: Vec::new(),
289 preference,
290 diagnostics: Vec::new(),
291 };
292 let usable = !markup.chars().any(is_sentinel);
295 let mut cursor = 0;
296 for token in scan(markup, true) {
297 match token {
298 Token::Escape(at) => {
299 prepared.markup.push_str(&markup[cursor..at]);
300 cursor = at + 1;
301 }
302 Token::Asset { range, name } => match registry.resolve(&name) {
303 Some(asset)
304 if usable
305 && prepared.tokens.len()
306 <= (SENTINEL_LAST - SENTINEL_BASE) as usize =>
307 {
308 let sentinel = char::from_u32(SENTINEL_BASE + prepared.tokens.len() as u32)
309 .expect("private-use code point");
310 prepared.markup.push_str(&markup[cursor..range.start]);
311 prepared.markup.push(sentinel);
312 prepared.tokens.push((sentinel, Arc::clone(asset)));
313 cursor = range.end;
314 }
315 Some(_) => prepared.diagnostics.push(Diagnostic {
316 offset: range.start,
317 token: markup[range].to_string(),
318 kind: DiagnosticKind::Reserved,
319 }),
320 None => prepared.diagnostics.push(Diagnostic {
321 offset: range.start,
322 token: markup[range].to_string(),
323 kind: DiagnosticKind::UnknownAsset(name),
324 }),
325 },
326 Token::Malformed(range) => prepared.diagnostics.push(Diagnostic {
327 offset: range.start,
328 token: markup[range].to_string(),
329 kind: DiagnosticKind::Malformed,
330 }),
331 }
332 }
333 prepared.markup.push_str(&markup[cursor..]);
334 prepared
335 }
336
337 pub fn markup(&self) -> &str {
339 &self.markup
340 }
341
342 pub fn diagnostics(&self) -> &[Diagnostic] {
344 &self.diagnostics
345 }
346
347 pub fn finish(&self, parsed: &Text) -> Text {
350 let plain = parsed.plain();
351 let mut edits = Vec::new();
352 for (at, c) in plain.char_indices() {
353 if !is_sentinel(c) {
354 continue;
355 }
356 if let Some((_, asset)) = self.tokens.iter().find(|(s, _)| *s == c) {
357 edits.push(Edit {
358 range: at..at + c.len_utf8(),
359 insert: Some(placeholder_with(
360 asset,
361 self.preference,
362 MicroMeta::new(asset),
363 )),
364 });
365 }
366 }
367 splice(parsed, &edits)
368 }
369}
370
371pub fn render_markup(
374 console: &Console,
375 markup: &str,
376 registry: &MicroRegistry,
377 preference: FallbackPreference,
378) -> (Text, Vec<Diagnostic>) {
379 let prepared = PreparedMarkup::new(markup, registry, preference);
380 let parsed = console.render_str(prepared.markup(), None);
381 (prepared.finish(&parsed), prepared.diagnostics)
382}
383
384pub fn markup_text(
387 markup: &str,
388 registry: &MicroRegistry,
389 emoji: bool,
390 preference: FallbackPreference,
391) -> rich::errors::Result<(Text, Vec<Diagnostic>)> {
392 let prepared = PreparedMarkup::new(markup, registry, preference);
393 let source = prepared.markup();
394 let parsed = if !source.contains('[') {
395 Text::new(if emoji {
396 rich::emoji::replace(source)
397 } else {
398 source.to_string()
399 })
400 } else if emoji {
401 rich::markup::render_emoji(source)?
402 } else {
403 rich::markup::render(source)?
404 };
405 Ok((prepared.finish(&parsed), prepared.diagnostics))
406}
407
408fn markdown_literal(source: &str) -> Vec<Range<usize>> {
414 use pulldown_cmark::{Event, LinkType, Options, Parser, Tag, TagEnd};
415 let parser = Parser::new_ext(source, Options::ENABLE_TABLES);
416 let mut ranges: Vec<Range<usize>> = parser
417 .reference_definitions()
418 .iter()
419 .map(|(_, def)| def.span.clone())
420 .collect();
421 let mut links: Vec<(Range<usize>, usize)> = Vec::new();
423 for (event, range) in parser.into_offset_iter() {
424 match event {
425 Event::Code(_) | Event::Start(Tag::CodeBlock(_)) => ranges.push(range.clone()),
426 Event::Start(Tag::Link {
427 link_type: LinkType::Autolink | LinkType::Email,
428 ..
429 }) => ranges.push(range.clone()),
430 Event::Start(Tag::Link { .. } | Tag::Image { .. }) => {
431 links.push((range.clone(), range.start + 1));
432 continue;
433 }
434 Event::End(TagEnd::Link | TagEnd::Image) => {
435 if let Some((whole, text_end)) = links.pop() {
438 ranges.push(text_end..whole.end);
439 if let Some(outer) = links.last_mut() {
440 outer.1 = outer.1.max(whole.end);
441 }
442 }
443 continue;
444 }
445 _ => {}
446 }
447 if let Some(open) = links.last_mut()
448 && !range.is_empty()
449 && range.end <= open.0.end
450 {
451 open.1 = open.1.max(range.end);
452 }
453 }
454 ranges
455}
456
457#[derive(Clone, Debug)]
490pub struct PreparedMarkdown {
491 source: String,
492 tokens: Vec<(char, Arc<MicroAsset>, MicroMeta)>,
494 preference: FallbackPreference,
495 diagnostics: Vec<Diagnostic>,
496}
497
498impl PreparedMarkdown {
499 pub fn new(source: &str, registry: &MicroRegistry, preference: FallbackPreference) -> Self {
500 let mut prepared = PreparedMarkdown {
501 source: String::with_capacity(source.len()),
502 tokens: Vec::new(),
503 preference,
504 diagnostics: Vec::new(),
505 };
506 if !source.contains(TOKEN_PREFIX) {
507 prepared.source.push_str(source);
508 return prepared;
509 }
510 let usable = !source.chars().any(is_sentinel);
511 let literal = markdown_literal(source);
512 let literal_at = |token: &Range<usize>| {
513 literal
514 .iter()
515 .any(|range| range.start < token.end && token.start < range.end)
516 };
517 let mut cursor = 0;
518 for token in scan(source, false) {
519 match token {
520 Token::Escape(_) => {}
522 Token::Asset { range, .. } | Token::Malformed(range) if literal_at(&range) => {}
523 Token::Asset { range, name } => match registry.resolve(&name) {
524 Some(asset)
525 if usable
526 && prepared.tokens.len()
527 <= (SENTINEL_LAST - SENTINEL_BASE) as usize =>
528 {
529 let sentinel = char::from_u32(SENTINEL_BASE + prepared.tokens.len() as u32)
530 .expect("private-use code point");
531 prepared.source.push_str(&source[cursor..range.start]);
532 prepared
533 .source
534 .extend(std::iter::repeat_n(sentinel, asset.cols()));
535 prepared
536 .tokens
537 .push((sentinel, Arc::clone(asset), MicroMeta::new(asset)));
538 cursor = range.end;
539 }
540 Some(_) => prepared.diagnostics.push(Diagnostic {
541 offset: range.start,
542 token: source[range].to_string(),
543 kind: DiagnosticKind::Reserved,
544 }),
545 None => prepared.diagnostics.push(Diagnostic {
546 offset: range.start,
547 token: source[range].to_string(),
548 kind: DiagnosticKind::UnknownAsset(name),
549 }),
550 },
551 Token::Malformed(range) => prepared.diagnostics.push(Diagnostic {
552 offset: range.start,
553 token: source[range].to_string(),
554 kind: DiagnosticKind::Malformed,
555 }),
556 }
557 }
558 prepared.source.push_str(&source[cursor..]);
559 prepared
560 }
561
562 pub fn source(&self) -> &str {
564 &self.source
565 }
566
567 pub fn has_assets(&self) -> bool {
569 !self.tokens.is_empty()
570 }
571
572 pub fn diagnostics(&self) -> &[Diagnostic] {
574 &self.diagnostics
575 }
576
577 pub fn view<R: Renderable>(&self, inner: R) -> MarkdownView<R> {
582 MarkdownView {
583 inner,
584 tokens: self.tokens.clone(),
585 preference: self.preference,
586 }
587 }
588}
589
590pub struct MarkdownView<R> {
592 inner: R,
593 tokens: Vec<(char, Arc<MicroAsset>, MicroMeta)>,
594 preference: FallbackPreference,
595}
596
597impl<R> MarkdownView<R> {
598 fn restore(&self, segment: Segment, out: &mut Vec<Segment>) {
600 if segment.control || !segment.text.chars().any(is_sentinel) {
601 out.push(segment);
602 return;
603 }
604 let text = segment.text.as_str();
605 let mut plain_start = 0;
606 let mut chars = text.char_indices().peekable();
607 while let Some((at, c)) = chars.next() {
608 if !is_sentinel(c) {
609 continue;
610 }
611 let mut count = 1;
612 while chars.peek().is_some_and(|(_, next)| *next == c) {
613 chars.next();
614 count += 1;
615 }
616 let end = chars.peek().map_or(text.len(), |(next, _)| *next);
617 if at > plain_start {
618 out.push(Segment::new(&text[plain_start..at], segment.style.clone()));
619 }
620 plain_start = end;
621 let blank = PAD_CELL.to_string().repeat(count);
622 let Some((_, asset, meta)) = self.tokens.iter().find(|(s, _, _)| *s == c) else {
623 out.push(Segment::new(blank, segment.style.clone()));
624 continue;
625 };
626 let tag = meta.to_style();
627 let style = Some(match &segment.style {
628 Some(style) => style.combine(&tag),
629 None => tag,
630 });
631 let cells = if count == meta.cols {
632 fallback_cells(asset, self.preference)
633 } else {
634 blank
635 };
636 out.push(Segment::new(cells, style));
637 }
638 if plain_start < text.len() {
639 out.push(Segment::new(&text[plain_start..], segment.style.clone()));
640 }
641 }
642}
643
644impl<R: Renderable> Renderable for MarkdownView<R> {
645 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
646 let segments = self.inner.rich_render(console, options);
647 if self.tokens.is_empty() {
648 return segments;
649 }
650 let mut out = Vec::with_capacity(segments.len());
651 for segment in segments {
652 self.restore(segment, &mut out);
653 }
654 out
655 }
656
657 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
658 self.inner.measure(console, options)
659 }
660}
661
662#[derive(Clone, Debug)]
666pub struct MicroTransform {
667 registry: Arc<MicroRegistry>,
668 preference: FallbackPreference,
669}
670
671impl MicroTransform {
672 pub fn new(registry: Arc<MicroRegistry>) -> Self {
673 MicroTransform {
674 registry,
675 preference: FallbackPreference::default(),
676 }
677 }
678
679 #[must_use]
680 pub fn preference(mut self, preference: FallbackPreference) -> Self {
681 self.preference = preference;
682 self
683 }
684}
685
686impl TextTransform for MicroTransform {
687 fn transform(&self, text: Text) -> Result<Text, PluginError> {
688 Ok(expand(&text, &self.registry, self.preference).0)
689 }
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695
696 #[test]
697 fn scanning() {
698 let tokens = scan(
699 r"a :micro:x/y: \:micro:z: :micro:Bad: [link=:micro:q:]t[/]",
700 true,
701 );
702 let kinds: Vec<String> = tokens
703 .iter()
704 .map(|t| match t {
705 Token::Escape(at) => format!("esc@{at}"),
706 Token::Asset { name, .. } => format!("asset {name}"),
707 Token::Malformed(r) => format!("bad@{}", r.start),
708 })
709 .collect();
710 assert_eq!(kinds, ["asset x/y", "esc@14", "bad@25"]);
711 }
712
713 #[test]
714 fn splice_moves_spans() {
715 let mut text = Text::new("ab:micro:x:cd");
716 text.stylize("red", 0, 13);
717 text.stylize("blue", 5, 7); text.stylize("green", 11, 13); let edits = [Edit {
720 range: 2..11,
721 insert: Some(Text::new("XY")),
722 }];
723 let out = splice(&text, &edits);
724 assert_eq!(out.plain(), "abXYcd");
725 let ranges: Vec<(usize, usize)> = out.spans().iter().map(|s| (s.start, s.end)).collect();
726 assert_eq!(ranges, [(0, 6), (2, 4), (4, 6)]);
727 }
728}