Skip to content

Commit a77336b

Browse files
committed
Implement inline macro handling for AsciiDoc-to-Markdown conversion
1 parent c7c6c6b commit a77336b

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed

xtask/src/publish/notes.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::{anyhow, bail};
22
use std::{
3+
borrow::Cow,
34
io::{BufRead, Lines},
45
iter::Peekable,
56
};
@@ -431,6 +432,141 @@ impl ListMarker {
431432
}
432433
}
433434

435+
fn process_inline_macros(line: &str) -> anyhow::Result<Cow<'_, str>> {
436+
let mut chars = line.char_indices();
437+
loop {
438+
let (start, end, a_macro) = match get_next_line_component(&mut chars) {
439+
Component::None => break,
440+
Component::Text => continue,
441+
Component::Macro(s, e, m) => (s, e, m),
442+
};
443+
let mut src = line.chars();
444+
let mut processed = String::new();
445+
for _ in 0..start {
446+
processed.push(src.next().unwrap());
447+
}
448+
processed.push_str(a_macro.process()?.as_str());
449+
for _ in start..end {
450+
let _ = src.next().unwrap();
451+
}
452+
let mut pos = end;
453+
454+
loop {
455+
let (start, end, a_macro) = match get_next_line_component(&mut chars) {
456+
Component::None => break,
457+
Component::Text => continue,
458+
Component::Macro(s, e, m) => (s, e, m),
459+
};
460+
for _ in pos..start {
461+
processed.push(src.next().unwrap());
462+
}
463+
processed.push_str(a_macro.process()?.as_str());
464+
for _ in start..end {
465+
let _ = src.next().unwrap();
466+
}
467+
pos = end;
468+
}
469+
for ch in src {
470+
processed.push(ch);
471+
}
472+
return Ok(Cow::Owned(processed));
473+
}
474+
Ok(Cow::Borrowed(line))
475+
}
476+
477+
fn get_next_line_component(chars: &mut std::str::CharIndices<'_>) -> Component {
478+
let (start, mut macro_name) = match chars.next() {
479+
None => return Component::None,
480+
Some((_, ch)) if ch == ' ' || !ch.is_ascii() => return Component::Text,
481+
Some((pos, ch)) => (pos, String::from(ch)),
482+
};
483+
loop {
484+
match chars.next() {
485+
None => return Component::None,
486+
Some((_, ch)) if ch == ' ' || !ch.is_ascii() => return Component::Text,
487+
Some((_, ':')) => break,
488+
Some((_, ch)) => macro_name.push(ch),
489+
}
490+
}
491+
492+
let mut macro_target = String::new();
493+
loop {
494+
match chars.next() {
495+
None => return Component::None,
496+
Some((_, ' ')) => return Component::Text,
497+
Some((_, '[')) => break,
498+
Some((_, ch)) => macro_target.push(ch),
499+
}
500+
}
501+
502+
let mut attr_value = String::new();
503+
let end = loop {
504+
match chars.next() {
505+
None => return Component::None,
506+
Some((pos, ']')) => break pos + 1,
507+
Some((_, ch)) => attr_value.push(ch),
508+
}
509+
};
510+
511+
Component::Macro(start, end, Macro::new(macro_name, macro_target, attr_value))
512+
}
513+
514+
enum Component {
515+
None,
516+
Text,
517+
Macro(usize, usize, Macro),
518+
}
519+
520+
struct Macro {
521+
name: String,
522+
target: String,
523+
attrs: String,
524+
}
525+
526+
impl Macro {
527+
fn new(name: String, target: String, attrs: String) -> Self {
528+
Self { name, target, attrs }
529+
}
530+
531+
fn process(&self) -> anyhow::Result<String> {
532+
let name = &self.name;
533+
let text = match name.as_str() {
534+
"https" => {
535+
let url = &self.target;
536+
let anchor_text = &self.attrs;
537+
format!("[{anchor_text}](https:{url})")
538+
}
539+
"image" => {
540+
let url = &self.target;
541+
let alt = &self.attrs;
542+
format!("![{alt}]({url})")
543+
}
544+
"kbd" => {
545+
let keys = self.attrs.split('+').map(|k| Cow::Owned(format!("<kbd>{k}</kbd>")));
546+
keys.collect::<Vec<_>>().join("+")
547+
}
548+
"pr" => {
549+
let pr = &self.target;
550+
let url = format!("https://github.com/rust-analyzer/rust-analyzer/pull/{pr}");
551+
format!("[`#{pr}`]({url})")
552+
}
553+
"commit" => {
554+
let hash = &self.target;
555+
let short = &hash[0..7];
556+
let url = format!("https://github.com/rust-analyzer/rust-analyzer/commit/{hash}");
557+
format!("[`{short}`]({url})")
558+
}
559+
"release" => {
560+
let date = &self.target;
561+
let url = format!("https://github.com/rust-analyzer/rust-analyzer/releases/{date}");
562+
format!("[`{date}`]({url})")
563+
}
564+
_ => bail!("macro not supported: {name}"),
565+
};
566+
Ok(text)
567+
}
568+
}
569+
434570
#[cfg(test)]
435571
mod tests {
436572
use super::*;
@@ -612,4 +748,50 @@ This is a plain listing.
612748

613749
assert_eq!(actual, expected);
614750
}
751+
752+
macro_rules! test_inline_macro_processing {
753+
($((
754+
$name:ident,
755+
$input:expr,
756+
$expected:expr
757+
),)*) => ($(
758+
#[test]
759+
fn $name() {
760+
let input = $input;
761+
let actual = process_inline_macros(&input).unwrap();
762+
let expected = $expected;
763+
assert_eq!(actual, expected)
764+
}
765+
)*);
766+
}
767+
768+
test_inline_macro_processing! {
769+
(inline_macro_processing_for_empty_line, "", ""),
770+
(inline_macro_processing_for_line_with_no_macro, "foo bar", "foo bar"),
771+
(
772+
inline_macro_processing_for_macro_in_line_start,
773+
"kbd::[Ctrl+T] foo",
774+
"<kbd>Ctrl</kbd>+<kbd>T</kbd> foo"
775+
),
776+
(
777+
inline_macro_processing_for_macro_in_line_end,
778+
"foo kbd::[Ctrl+T]",
779+
"foo <kbd>Ctrl</kbd>+<kbd>T</kbd>"
780+
),
781+
(
782+
inline_macro_processing_for_macro_in_the_middle_of_line,
783+
"foo kbd::[Ctrl+T] foo",
784+
"foo <kbd>Ctrl</kbd>+<kbd>T</kbd> foo"
785+
),
786+
(
787+
inline_macro_processing_for_several_macros,
788+
"foo kbd::[Ctrl+T] foo kbd::[Enter] foo",
789+
"foo <kbd>Ctrl</kbd>+<kbd>T</kbd> foo <kbd>Enter</kbd> foo"
790+
),
791+
(
792+
inline_macro_processing_for_several_macros_without_text_in_between,
793+
"foo kbd::[Ctrl+T]kbd::[Enter] foo",
794+
"foo <kbd>Ctrl</kbd>+<kbd>T</kbd><kbd>Enter</kbd> foo"
795+
),
796+
}
615797
}

0 commit comments

Comments
 (0)