| author | |
| committer | |
| log | 4ab8d7af06ad7ed6563556540f679b77f9374120 |
| tree | c2b78d02803810c0d759b2ff799cc05cfe7791e3 |
| parent | 06e56b3aa60a307cbf8c6dd1f7a2c59c55ba0de9 |
| signature |
16 files changed, 217 insertions(+), 46 deletions(-)
README.md+14-14| ... | @@ -1,19 +1,18 @@ | ... | @@ -1,19 +1,18 @@ |
| 1 | # Markodown | 1 | # Markodown |
| 2 | 2 | ||
| 3 | > STATUS: Markodown is not yet in use at paperclover.net. However, the API is | ||
| 4 | > complete and the library is functional. Give it a try! | ||
| 5 | |||
| 6 | This is a weird markup language that combines features of [Markdown] and | 3 | This is a weird markup language that combines features of [Markdown] and |
| 7 | [Marko]. You can think of this as an alternative universe to MDX. Since Marko | 4 | [Marko]. You can think of this as an alternative universe to MDX. Since Marko |
| 8 | components are really easy to write, it makes this a great tool for writing | 5 | components are really easy to write, it makes this a great tool for writing |
| 9 | interactive blog posts. Markdown is compiled directly into `.marko` syntax, | 6 | interactive blog posts. Markdown is compiled directly into `.marko` syntax, |
| 10 | leveraging the existing ecosystem. | 7 | leveraging the existing ecosystem. |
| 11 | 8 | ||
| 9 | Markodown is used in production for | ||
| 10 | [my blog posts on paperclover.net](https://paperclover.net), where I ported my | ||
| 11 | posts from MDX to it. | ||
| 12 | |||
| 12 | [Markdown]: https://en.wikipedia.org/wiki/Markdown | 13 | [Markdown]: https://en.wikipedia.org/wiki/Markdown |
| 13 | [Marko]: https://markojs.com/ | 14 | [Marko]: https://markojs.com/ |
| 14 | 15 | ||
| 15 | > **CONTENTS**: | ||
| 16 | > | ||
| 17 | > - [Usage](#usage) | 16 | > - [Usage](#usage) |
| 18 | > - [Components](#components) | 17 | > - [Components](#components) |
| 19 | > - [Outline / Table of Contents](#outline-table-of-contents) | 18 | > - [Outline / Table of Contents](#outline-table-of-contents) |
| ... | @@ -24,7 +23,7 @@ leveraging the existing ecosystem. | ... | @@ -24,7 +23,7 @@ leveraging the existing ecosystem. |
| 24 | > - [Config](#config) | 23 | > - [Config](#config) |
| 25 | > - [Frontmatter Layout Configuration](#frontmatter-layout-configuration) | 24 | > - [Frontmatter Layout Configuration](#frontmatter-layout-configuration) |
| 26 | 25 | ||
| 27 | Here's a glance at how things look. Complete example documents in <./examples> | 26 | Here's a glance at how things look. Complete example documents in `examples`. |
| 28 | 27 | ||
| 29 | ```` | 28 | ```` |
| 30 | --- | 29 | --- |
| ... | @@ -271,21 +270,22 @@ import { Heading } from "@clo/markodown"; | ... | @@ -271,21 +270,22 @@ import { Heading } from "@clo/markodown"; |
| 271 | 270 | ||
| 272 | export interface Input { | 271 | export interface Input { |
| 273 | content: Marko.Body; | 272 | content: Marko.Body; |
| 274 | 273 | // Markdown scans for headings (h1..h6) | |
| 275 | /** Markdown scans for headings (h1..h6) */ | ||
| 276 | outline: Heading[]; | 274 | outline: Heading[]; |
| 277 | /** This is the namespace import of the main document. | 275 | // This is the namespace import of the main document. |
| 278 | * You can reflect frontmatter, or do whatever with this. */ | 276 | // You can reflect frontmatter, or do whatever with this. |
| 279 | module: Record<string, unknown>; | 277 | module: Record<string, unknown>; |
| 280 | } | 278 | } |
| 281 | 279 | ||
| 282 | <main> | 280 | <main> |
| 283 | <h1>${input.module.title ?? "Blog Post"}</h1> | 281 | <h1>${input.module.title ?? "Blog Post"}</h1> |
| 284 | <aside> | 282 | <aside> |
| 285 | <for|heading| of=input.outline> | 283 | <ul> |
| 286 | // heading content includes formatting, even custom tags. | 284 | <for|heading| of=input.outline> |
| 287 | <li><a href=`#${heading.id}`><${heading.content}/></a></li> | 285 | // heading content includes formatting, even custom tags. |
| 288 | </for> | 286 | <li><a href=`#${heading.id}`><${heading.content}/></a></li> |
| 287 | </for> | ||
| 288 | </ul> | ||
| 289 | </aside> | 289 | </aside> |
| 290 | 290 | ||
| 291 | <${input.content} /> | 291 | <${input.content} /> |
lib/jsr.json+1-1| ... | @@ -1,6 +1,6 @@ | ... | @@ -1,6 +1,6 @@ |
| 1 | { | 1 | { |
| 2 | "name": "@clo/markodown", | 2 | "name": "@clo/markodown", |
| 3 | "version": "1.0.0-rc.9", | 3 | "version": "1.0.0", |
| 4 | "license": "ISC", | 4 | "license": "ISC", |
| 5 | "exports": { | 5 | "exports": { |
| 6 | ".": "./mod.ts", | 6 | ".": "./mod.ts", |
lib/mod.ts+23-1| ... | @@ -29,7 +29,29 @@ export function transform(options: TransformOptions): Transformed { | ... | @@ -29,7 +29,29 @@ export function transform(options: TransformOptions): Transformed { |
| 29 | ); | 29 | ); |
| 30 | } | 30 | } |
| 31 | 31 | ||
| 32 | /** Converts a flat document outline into a nested tree. */ | 32 | /** |
| 33 | * Converts a flat document outline into a nested tree. You can consume this | ||
| 34 | * tree with a recursive Marko component: | ||
| 35 | |||
| 36 | * ```marko | ||
| 37 | * <define/Recurse|input: HeadingTree|> | ||
| 38 | * <a href=`#${input.id}`><${input.content} /></a> | ||
| 39 | |||
| 40 | * <if=input.children.length> | ||
| 41 | * <ul><for|item| of=input.children> | ||
| 42 | * <li><Recurse ...item /></li> | ||
| 43 | * </></ul> | ||
| 44 | * </> | ||
| 45 | * </> | ||
| 46 | * | ||
| 47 | * <div#toc> | ||
| 48 | * <h2>Contents:</h2> | ||
| 49 | * <for|item| of=groups> | ||
| 50 | * <li><Recurse ...item /></li> | ||
| 51 | * </> | ||
| 52 | * </div> | ||
| 53 | * ``` | ||
| 54 | */ | ||
| 33 | export function outlineToTree(outline: Heading[]): HeadingTree[] { | 55 | export function outlineToTree(outline: Heading[]): HeadingTree[] { |
| 34 | const root: HeadingTree[] = []; | 56 | const root: HeadingTree[] = []; |
| 35 | const stack: HeadingTree[] = []; | 57 | const stack: HeadingTree[] = []; |
src/lib.rs+86-9| ... | @@ -234,11 +234,11 @@ pub fn transform( | ... | @@ -234,11 +234,11 @@ pub fn transform( |
| 234 | 234 | ||
| 235 | if let Some(self_path) = self_import { | 235 | if let Some(self_path) = self_import { |
| 236 | text = format!( | 236 | text = format!( |
| 237 | "import Layout__markodown__ from \"{layout_path}\";\nimport * as LayoutModule__markodown__ from \"{layout_path}\";\nimport * as self__markodown__ from \"{self_path}\";\n{boilerplate}\n{hoisted}\n<Layout__markodown__ module=self__markodown__ outline={outline_array}>\n{text}</>" | 237 | "import Layout__markodown__ from \"{layout_path}\";\nexport * as layout from \"{layout_path}\";\nimport * as LayoutModule__markodown__ from \"{layout_path}\";\nimport * as self__markodown__ from \"{self_path}\";\n{boilerplate}\n{hoisted}\n<Layout__markodown__ module=self__markodown__ outline={outline_array}>\n{text}</>" |
| 238 | ); | 238 | ); |
| 239 | } else { | 239 | } else { |
| 240 | text = format!( | 240 | text = format!( |
| 241 | "import Layout__markodown__ from \"{layout_path}\";\nimport * as LayoutModule__markodown__ from \"{layout_path}\";\n{boilerplate}\n{hoisted}\n<Layout__markodown__ module=null outline={outline_array}>\n{text}</>" | 241 | "import Layout__markodown__ from \"{layout_path}\";\nexport * as layout from \"{layout_path}\";\nimport * as LayoutModule__markodown__ from \"{layout_path}\";\n{boilerplate}\n{hoisted}\n<Layout__markodown__ module=null outline={outline_array}>\n{text}</>" |
| 242 | ); | 242 | ); |
| 243 | } | 243 | } |
| 244 | } else { | 244 | } else { |
| ... | @@ -598,17 +598,70 @@ mod tests { | ... | @@ -598,17 +598,70 @@ mod tests { |
| 598 | // Frontmatter layout field | 598 | // Frontmatter layout field |
| 599 | // ------------------------------------------------------------------------- | 599 | // ------------------------------------------------------------------------- |
| 600 | 600 | ||
| 601 | // Quoted id= attribute on MarkoBlockComplete (<tag>content</>) | ||
| 601 | #[test] | 602 | #[test] |
| 602 | fn frontmatter_layout_not_exported_as_const() { | 603 | fn marko_block_complete_heading_single_quoted_id_decoded() { |
| 603 | let source = "---\nlayout: ./l.marko\ntitle: Hi\n---\n\ntext"; | 604 | let out = run_with_layout("<h2 id='real-world-pitfalls'>A heading</>", "./l.marko"); |
| 604 | let out = run(source); | ||
| 605 | assert!( | 605 | assert!( |
| 606 | !out.contains("export const layout"), | 606 | out.contains("id: 'real-world-pitfalls'"), |
| 607 | "layout key must not be exported" | 607 | "outline id must not contain the attribute quotes: {out}" |
| 608 | ); | 608 | ); |
| 609 | assert!( | 609 | assert!( |
| 610 | out.contains("export const title"), | 610 | !out.contains("''real-world-pitfalls''"), |
| 611 | "other fields must still export" | 611 | "outline array must not contain raw single-quoted attribute value" |
| 612 | ); | ||
| 613 | } | ||
| 614 | |||
| 615 | #[test] | ||
| 616 | fn marko_block_complete_heading_double_quoted_id_decoded() { | ||
| 617 | let out = run_with_layout("<h2 id=\"real-world-pitfalls\">A heading</>", "./l.marko"); | ||
| 618 | assert!( | ||
| 619 | out.contains("id: 'real-world-pitfalls'"), | ||
| 620 | "outline id must not contain the attribute double quotes: {out}" | ||
| 621 | ); | ||
| 622 | } | ||
| 623 | |||
| 624 | // Quoted id= attribute on MarkoOpen (<tag>\ncontent\n</>) | ||
| 625 | #[test] | ||
| 626 | fn marko_open_heading_single_quoted_id_decoded() { | ||
| 627 | let out = run_with_layout("<h2 id='real-world-pitfalls'>\nA heading\n</>", "./l.marko"); | ||
| 628 | assert!( | ||
| 629 | out.contains("id: 'real-world-pitfalls'"), | ||
| 630 | "multiline MarkoOpen heading: outline id must not contain attribute quotes: {out}" | ||
| 631 | ); | ||
| 632 | } | ||
| 633 | |||
| 634 | #[test] | ||
| 635 | fn marko_open_heading_double_quoted_id_decoded() { | ||
| 636 | let out = run_with_layout( | ||
| 637 | "<h2 id=\"real-world-pitfalls\">\nA heading\n</>", | ||
| 638 | "./l.marko", | ||
| 639 | ); | ||
| 640 | assert!( | ||
| 641 | out.contains("id: 'real-world-pitfalls'"), | ||
| 642 | "multiline MarkoOpen heading: outline id must not contain attribute double quotes: {out}" | ||
| 643 | ); | ||
| 644 | } | ||
| 645 | |||
| 646 | // Quoted id= attribute on MarkoOpenWithText (<tag> text on same line, close elsewhere) | ||
| 647 | #[test] | ||
| 648 | fn marko_open_with_text_heading_single_quoted_id_decoded() { | ||
| 649 | let out = run_with_layout("<h2 id='real-world-pitfalls'> A heading\n</>", "./l.marko"); | ||
| 650 | assert!( | ||
| 651 | out.contains("id: 'real-world-pitfalls'"), | ||
| 652 | "MarkoOpenWithText heading: outline id must not contain attribute quotes: {out}" | ||
| 653 | ); | ||
| 654 | } | ||
| 655 | |||
| 656 | #[test] | ||
| 657 | fn marko_open_with_text_heading_double_quoted_id_decoded() { | ||
| 658 | let out = run_with_layout( | ||
| 659 | "<h2 id=\"real-world-pitfalls\"> A heading\n</>", | ||
| 660 | "./l.marko", | ||
| 661 | ); | ||
| 662 | assert!( | ||
| 663 | out.contains("id: 'real-world-pitfalls'"), | ||
| 664 | "MarkoOpenWithText heading: outline id must not contain attribute double quotes: {out}" | ||
| 612 | ); | 665 | ); |
| 613 | } | 666 | } |
| 614 | 667 | ||
| ... | @@ -712,6 +765,30 @@ mod tests { | ... | @@ -712,6 +765,30 @@ mod tests { |
| 712 | assert!(out.contains("level=2"), "level attr should be added"); | 765 | assert!(out.contains("level=2"), "level attr should be added"); |
| 713 | } | 766 | } |
| 714 | 767 | ||
| 768 | #[test] | ||
| 769 | fn marko_heading_quoted_id_attr_decoded_in_outline() { | ||
| 770 | // id='real-world-pitfalls' (single-quoted attr) must be decoded to | ||
| 771 | // real-world-pitfalls (no quotes) before being placed in the outline array | ||
| 772 | let out = run_with_layout("<h2 id='real-world-pitfalls'>A heading</>", "./l.marko"); | ||
| 773 | assert!( | ||
| 774 | out.contains("id: 'real-world-pitfalls'"), | ||
| 775 | "outline id must not contain the attribute quotes: {out}" | ||
| 776 | ); | ||
| 777 | assert!( | ||
| 778 | !out.contains("''real-world-pitfalls''"), | ||
| 779 | "outline array must not contain raw single-quoted attribute value" | ||
| 780 | ); | ||
| 781 | } | ||
| 782 | |||
| 783 | #[test] | ||
| 784 | fn marko_heading_double_quoted_id_attr_decoded_in_outline() { | ||
| 785 | let out = run_with_layout("<h2 id=\"real-world-pitfalls\">A heading</>", "./l.marko"); | ||
| 786 | assert!( | ||
| 787 | out.contains("id: 'real-world-pitfalls'"), | ||
| 788 | "outline id must not contain the attribute quotes: {out}" | ||
| 789 | ); | ||
| 790 | } | ||
| 791 | |||
| 715 | #[test] | 792 | #[test] |
| 716 | fn heading_content_hoisted_into_define() { | 793 | fn heading_content_hoisted_into_define() { |
| 717 | let out = run_with_layout("# **bold** heading", "./l.marko"); | 794 | let out = run_with_layout("# **bold** heading", "./l.marko"); |
src/outline.rs+37-18| ... | @@ -13,6 +13,21 @@ use crate::marko_ast::AttributeValue; | ... | @@ -13,6 +13,21 @@ use crate::marko_ast::AttributeValue; |
| 13 | use crate::plugin::tags::{MarkoBlockComplete, MarkoOpen, MarkoOpenWithText}; | 13 | use crate::plugin::tags::{MarkoBlockComplete, MarkoOpen, MarkoOpenWithText}; |
| 14 | use crate::typescript::parse_expr_extra; | 14 | use crate::typescript::parse_expr_extra; |
| 15 | 15 | ||
| 16 | /// Decode a static id attribute value, stripping quotes and unescaping if needed. | ||
| 17 | fn decode_static_id(src: &str, span: Span, is_quoted: bool) -> String { | ||
| 18 | let string = &src[span.start as usize..span.end as usize]; | ||
| 19 | if is_quoted { | ||
| 20 | let mut allocator = Allocator::new(); | ||
| 21 | let expr = parse_expr_extra(string, 0, &mut allocator); | ||
| 22 | match &expr { | ||
| 23 | Ok(oxc_ast::ast::Expression::StringLiteral(literal)) => literal.value.to_string(), | ||
| 24 | _ => panic!("verified beforehand as a string literal"), | ||
| 25 | } | ||
| 26 | } else { | ||
| 27 | string.to_owned() | ||
| 28 | } | ||
| 29 | } | ||
| 30 | |||
| 16 | /// A heading entry for the outline, with content reference for hoisting. | 31 | /// A heading entry for the outline, with content reference for hoisting. |
| 17 | #[derive(Debug, Clone)] | 32 | #[derive(Debug, Clone)] |
| 18 | pub struct HeadingEntry { | 33 | pub struct HeadingEntry { |
| ... | @@ -248,19 +263,7 @@ fn collect_recursive( | ... | @@ -248,19 +263,7 @@ fn collect_recursive( |
| 248 | let tag_span = open.open.as_ref().tag_name_span(); | 263 | let tag_span = open.open.as_ref().tag_name_span(); |
| 249 | let existing_id = match id_info { | 264 | let existing_id = match id_info { |
| 250 | AttributeValue::Static { span, is_quoted } => { | 265 | AttributeValue::Static { span, is_quoted } => { |
| 251 | let string = &open.open.src[span.start as usize..span.end as usize]; | 266 | Some(decode_static_id(&open.open.src, span, is_quoted)) |
| 252 | Some(if is_quoted { | ||
| 253 | let mut allocator = Allocator::new(); | ||
| 254 | let expr = parse_expr_extra(string, 0, &mut allocator); | ||
| 255 | match &expr { | ||
| 256 | Ok(oxc_ast::ast::Expression::StringLiteral(literal)) => { | ||
| 257 | literal.value.into_string() | ||
| 258 | } | ||
| 259 | _ => panic!("verified beforehand as a string literal"), | ||
| 260 | } | ||
| 261 | } else { | ||
| 262 | string.to_owned() | ||
| 263 | }) | ||
| 264 | } | 267 | } |
| 265 | _ => None, | 268 | _ => None, |
| 266 | }; | 269 | }; |
| ... | @@ -338,8 +341,8 @@ fn collect_recursive( | ... | @@ -338,8 +341,8 @@ fn collect_recursive( |
| 338 | let id_info = open.open.id; | 341 | let id_info = open.open.id; |
| 339 | let tag_span = open.open.as_ref().tag_name_span(); | 342 | let tag_span = open.open.as_ref().tag_name_span(); |
| 340 | let existing_id = match id_info { | 343 | let existing_id = match id_info { |
| 341 | AttributeValue::Static { span, .. } => { | 344 | AttributeValue::Static { span, is_quoted } => { |
| 342 | Some(open.open.src[span.start as usize..span.end as usize].to_string()) | 345 | Some(decode_static_id(&open.open.src, span, is_quoted)) |
| 343 | } | 346 | } |
| 344 | _ => None, | 347 | _ => None, |
| 345 | }; | 348 | }; |
| ... | @@ -417,8 +420,8 @@ fn collect_recursive( | ... | @@ -417,8 +420,8 @@ fn collect_recursive( |
| 417 | let id_info = block.open.id; | 420 | let id_info = block.open.id; |
| 418 | let tag_span = block.open.as_ref().tag_name_span(); | 421 | let tag_span = block.open.as_ref().tag_name_span(); |
| 419 | let existing_id = match id_info { | 422 | let existing_id = match id_info { |
| 420 | AttributeValue::Static { span, .. } => { | 423 | AttributeValue::Static { span, is_quoted } => { |
| 421 | Some(block.open.src[span.start as usize..span.end as usize].to_string()) | 424 | Some(decode_static_id(&block.open.src, span, is_quoted)) |
| 422 | } | 425 | } |
| 423 | _ => None, | 426 | _ => None, |
| 424 | }; | 427 | }; |
| ... | @@ -519,9 +522,10 @@ pub fn format_outline_array(headings: &[HeadingEntry]) -> String { | ... | @@ -519,9 +522,10 @@ pub fn format_outline_array(headings: &[HeadingEntry]) -> String { |
| 519 | let entries: Vec<String> = headings | 522 | let entries: Vec<String> = headings |
| 520 | .iter() | 523 | .iter() |
| 521 | .map(|h| { | 524 | .map(|h| { |
| 525 | let escaped_id = h.id.replace('\\', "\\\\").replace('\'', "\\'"); | ||
| 522 | format!( | 526 | format!( |
| 523 | "{{ level: {}, id: '{}', content: {} }}", | 527 | "{{ level: {}, id: '{}', content: {} }}", |
| 524 | h.level, h.id, h.component_name | 528 | h.level, escaped_id, h.component_name |
| 525 | ) | 529 | ) |
| 526 | }) | 530 | }) |
| 527 | .collect(); | 531 | .collect(); |
| ... | @@ -585,4 +589,19 @@ mod tests { | ... | @@ -585,4 +589,19 @@ mod tests { |
| 585 | assert!(result.contains("id: 'hello'")); | 589 | assert!(result.contains("id: 'hello'")); |
| 586 | assert!(result.contains("content: Heading_1__markodown__")); | 590 | assert!(result.contains("content: Heading_1__markodown__")); |
| 587 | } | 591 | } |
| 592 | |||
| 593 | #[test] | ||
| 594 | fn test_format_outline_array_escapes_single_quotes() { | ||
| 595 | let headings = vec![HeadingEntry { | ||
| 596 | level: 2, | ||
| 597 | id: "it's-here".to_string(), | ||
| 598 | text: "It's here".to_string(), | ||
| 599 | component_name: "Heading_1__markodown__".to_string(), | ||
| 600 | }]; | ||
| 601 | let result = format_outline_array(&headings); | ||
| 602 | assert!( | ||
| 603 | result.contains(r"id: 'it\'s-here'"), | ||
| 604 | "single quote in id must be escaped: {result}" | ||
| 605 | ); | ||
| 606 | } | ||
| 588 | } | 607 | } |
src/plugin/frontmatter.rs+8-2| ... | @@ -7,10 +7,16 @@ use crate::plugin::{get_line_raw, ErrorBlock, StatementBlock}; | ... | @@ -7,10 +7,16 @@ use crate::plugin::{get_line_raw, ErrorBlock, StatementBlock}; |
| 7 | /// Parse frontmatter (---) at document start | 7 | /// Parse frontmatter (---) at document start |
| 8 | pub(crate) struct Rule; | 8 | pub(crate) struct Rule; |
| 9 | 9 | ||
| 10 | /// Strip '//' comments from YAML content | 10 | /// Strip '//' comments from YAML content (only when // starts the line) |
| 11 | fn strip_js_comments(yaml: &str) -> String { | 11 | fn strip_js_comments(yaml: &str) -> String { |
| 12 | yaml.lines() | 12 | yaml.lines() |
| 13 | .map(|line| line.find("//").map_or(line, |pos| &line[..pos])) | 13 | .map(|line| { |
| 14 | if line.trim_start().starts_with("//") { | ||
| 15 | "" | ||
| 16 | } else { | ||
| 17 | line | ||
| 18 | } | ||
| 19 | }) | ||
| 14 | .collect::<Vec<_>>() | 20 | .collect::<Vec<_>>() |
| 15 | .join("\n") | 21 | .join("\n") |
| 16 | } | 22 | } |
src/plugin/tags.rs+11-1| ... | @@ -238,7 +238,17 @@ impl BlockRule for Rule { | ... | @@ -238,7 +238,17 @@ impl BlockRule for Rule { |
| 238 | // Check if the same-line content contains a matching close tag | 238 | // Check if the same-line content contains a matching close tag |
| 239 | if let Some((content, close_tag)) = find_same_line_close(text, open.tag_name()) | 239 | if let Some((content, close_tag)) = find_same_line_close(text, open.tag_name()) |
| 240 | { | 240 | { |
| 241 | // Complete tag on one line: <tag>content</tag> | 241 | // Complete tag on one line: <tag>content</tag>. |
| 242 | // If the previous line is non-empty prose (not another tag), this | ||
| 243 | // self-contained tag is inline content within a paragraph - let | ||
| 244 | // the paragraph rule handle it instead. | ||
| 245 | if state.line > 0 { | ||
| 246 | let prev_line = get_line_raw(state, state.line - 1).trim(); | ||
| 247 | if !prev_line.is_empty() && !prev_line.starts_with('<') { | ||
| 248 | return None; | ||
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 242 | let content = content.trim(); | 252 | let content = content.trim(); |
| 243 | 253 | ||
| 244 | // Calculate byte offset for source mapping | 254 | // Calculate byte offset for source mapping |
tests/fixtures.rs+7| ... | @@ -283,3 +283,10 @@ fn fixture_31_import_with_markdown_content() { | ... | @@ -283,3 +283,10 @@ fn fixture_31_import_with_markdown_content() { |
| 283 | // markdown link syntax [text](url) as a TypeScript array expression | 283 | // markdown link syntax [text](url) as a TypeScript array expression |
| 284 | run_fixture("31-import-with-markdown-content"); | 284 | run_fixture("31-import-with-markdown-content"); |
| 285 | } | 285 | } |
| 286 | |||
| 287 | #[test] | ||
| 288 | fn fixture_32_inline_tag_in_paragraph() { | ||
| 289 | // regression: a self-contained inline tag (<b>text</b>) on its own line within a paragraph | ||
| 290 | // was being consumed by the block tag rule, splitting one paragraph into three | ||
| 291 | run_fixture("32-inline-tag-in-paragraph"); | ||
| 292 | } |
tests/fixtures/23-outline-extracting.marko+1| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import Layout__markodown__ from "./layout.marko"; | 1 | import Layout__markodown__ from "./layout.marko"; |
| 2 | export * as layout from "./layout.marko"; | ||
| 2 | import * as LayoutModule__markodown__ from "./layout.marko"; | 3 | import * as LayoutModule__markodown__ from "./layout.marko"; |
| 3 | <define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|> | 4 | <define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|> |
| 4 | <${'h' + level} ...attrs><${content} /></> | 5 | <${'h' + level} ...attrs><${content} /></> |
tests/fixtures/27-frontmatter-layout.marko+1| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import Layout__markodown__ from "./page-layout.marko"; | 1 | import Layout__markodown__ from "./page-layout.marko"; |
| 2 | export * as layout from "./page-layout.marko"; | ||
| 2 | import * as LayoutModule__markodown__ from "./page-layout.marko"; | 3 | import * as LayoutModule__markodown__ from "./page-layout.marko"; |
| 3 | <define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|> | 4 | <define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|> |
| 4 | <${'h' + level} ...attrs><${content} /></> | 5 | <${'h' + level} ...attrs><${content} /></> |
tests/fixtures/28-outline-self-import.marko+1| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import Layout__markodown__ from "./layout.marko"; | 1 | import Layout__markodown__ from "./layout.marko"; |
| 2 | export * as layout from "./layout.marko"; | ||
| 2 | import * as LayoutModule__markodown__ from "./layout.marko"; | 3 | import * as LayoutModule__markodown__ from "./layout.marko"; |
| 3 | import * as self__markodown__ from "./self.marko"; | 4 | import * as self__markodown__ from "./self.marko"; |
| 4 | <define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|> | 5 | <define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|> |
tests/fixtures/29-layout-all-components.marko+1| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import Layout__markodown__ from "./layout.marko"; | 1 | import Layout__markodown__ from "./layout.marko"; |
| 2 | export * as layout from "./layout.marko"; | ||
| 2 | import * as LayoutModule__markodown__ from "./layout.marko"; | 3 | import * as LayoutModule__markodown__ from "./layout.marko"; |
| 3 | <define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|> | 4 | <define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|> |
| 4 | <${'h' + level} ...attrs><${content} /></> | 5 | <${'h' + level} ...attrs><${content} /></> |
tests/fixtures/30-layout-selective-components.marko+1| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import Layout__markodown__ from "./layout.marko"; | 1 | import Layout__markodown__ from "./layout.marko"; |
| 2 | export * as layout from "./layout.marko"; | ||
| 2 | import * as LayoutModule__markodown__ from "./layout.marko"; | 3 | import * as LayoutModule__markodown__ from "./layout.marko"; |
| 3 | <define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|> | 4 | <define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|> |
| 4 | <${'h' + level} ...attrs><${content} /></> | 5 | <${'h' + level} ...attrs><${content} /></> |
tests/fixtures/32-inline-tag-in-paragraph.marko created+7| ... | @@ -0,0 +1,7 @@ | ||
| 1 | <p>xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx | ||
| 2 | xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx.</p> | ||
| 3 | <p>xxx xxxxx xx xxx xx xxxx xxxxxxxxxx xxx xxx xxxx xxx xxxxxxxxxx, | ||
| 4 | <b>“xxxxxx”</b> xxxxxxxxxx xxx <b>“xxxxxx”</b> | ||
| 5 | xxxxxxxxxx. xxxxxx xxxxxxxxxx xxx’x xxxx <code>xxxxxxxx</code>, <code>xxxxxxxxx</code>.</p> | ||
| 6 | <p>xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx | ||
| 7 | xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx.</p> | ||
tests/fixtures/32-inline-tag-in-paragraph.mdo created+9| ... | @@ -0,0 +1,9 @@ | ||
| 1 | xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx | ||
| 2 | xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx. | ||
| 3 | |||
| 4 | xxx xxxxx xx xxx xx xxxx xxxxxxxxxx xxx xxx xxxx xxx xxxxxxxxxx, | ||
| 5 | <b>"xxxxxx"</b> xxxxxxxxxx xxx <b>"xxxxxx"</b> | ||
| 6 | xxxxxxxxxx. xxxxxx xxxxxxxxxx xxx'x xxxx `xxxxxxxx`, `xxxxxxxxx`. | ||
| 7 | |||
| 8 | xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx | ||
| 9 | xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx. | ||
wtf.mdo created+9| ... | @@ -0,0 +1,9 @@ | ||
| 1 | xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx | ||
| 2 | xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx. | ||
| 3 | |||
| 4 | xxx xxxxx xx xxx xx xxxx xxxxxxxxxx xxx xxx xxxx xxx xxxxxxxxxx, | ||
| 5 | <b>"xxxxxx"</b> xxxxxxxxxx xxx <b>"xxxxxx"</b> | ||
| 6 | xxxxxxxxxx. xxxxxx xxxxxxxxxx xxx'x xxxx `xxxxxxxx`, `xxxxxxxxx`. | ||
| 7 | |||
| 8 | xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx | ||
| 9 | xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx. | ||