authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-17 02:08:19-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-18 02:46:06-08:00
log4ab8d7af06ad7ed6563556540f679b77f9374120
treec2b78d02803810c0d759b2ff799cc05cfe7791e3
parent06e56b3aa60a307cbf8c6dd1f7a2c59c55ba0de9
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

fix: little cheecky outline bug


16 files changed, 217 insertions(+), 46 deletions(-)

README.md+14-14
...@@ -1,19 +1,18 @@...@@ -1,19 +1,18 @@
1# Markodown1# Markodown
22
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
6This is a weird markup language that combines features of [Markdown] and3This 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 Marko4[Marko]. You can think of this as an alternative universe to MDX. Since Marko
8components are really easy to write, it makes this a great tool for writing5components are really easy to write, it makes this a great tool for writing
9interactive blog posts. Markdown is compiled directly into `.marko` syntax,6interactive blog posts. Markdown is compiled directly into `.marko` syntax,
10leveraging the existing ecosystem.7leveraging the existing ecosystem.
118
9Markodown is used in production for
10[my blog posts on paperclover.net](https://paperclover.net), where I ported my
11posts from MDX to it.
12
12[Markdown]: https://en.wikipedia.org/wiki/Markdown13[Markdown]: https://en.wikipedia.org/wiki/Markdown
13[Marko]: https://markojs.com/14[Marko]: https://markojs.com/
1415
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)
2625
27Here's a glance at how things look. Complete example documents in <./examples>26Here's a glance at how things look. Complete example documents in `examples`.
2827
29````28````
30---29---
...@@ -271,21 +270,22 @@ import { Heading } from "@clo/markodown";...@@ -271,21 +270,22 @@ import { Heading } from "@clo/markodown";
271270
272export interface Input {271export 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}
281279
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>
290290
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}
3131
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 */
33export function outlineToTree(outline: Heading[]): HeadingTree[] {55export 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(
234234
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 field598 // Frontmatter layout field
599 // -------------------------------------------------------------------------599 // -------------------------------------------------------------------------
600600
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 }
614667
...@@ -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 }
714767
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;
13use crate::plugin::tags::{MarkoBlockComplete, MarkoOpen, MarkoOpenWithText};13use crate::plugin::tags::{MarkoBlockComplete, MarkoOpen, MarkoOpenWithText};
14use crate::typescript::parse_expr_extra;14use crate::typescript::parse_expr_extra;
1515
16/// Decode a static id attribute value, stripping quotes and unescaping if needed.
17fn 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)]
18pub struct HeadingEntry {33pub 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> = headings522 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_name528 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 start7/// Parse frontmatter (---) at document start
8pub(crate) struct Rule;8pub(crate) struct Rule;
99
10/// Strip '//' comments from YAML content10/// Strip '//' comments from YAML content (only when // starts the line)
11fn strip_js_comments(yaml: &str) -> String {11fn 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 tag238 // 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();
243253
244 // Calculate byte offset for source mapping254 // 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 expression283 // 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]
288fn 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 @@
1import Layout__markodown__ from "./layout.marko";1import Layout__markodown__ from "./layout.marko";
2export * as layout from "./layout.marko";
2import * as LayoutModule__markodown__ from "./layout.marko";3import * 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 @@
1import Layout__markodown__ from "./page-layout.marko";1import Layout__markodown__ from "./page-layout.marko";
2export * as layout from "./page-layout.marko";
2import * as LayoutModule__markodown__ from "./page-layout.marko";3import * 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 @@
1import Layout__markodown__ from "./layout.marko";1import Layout__markodown__ from "./layout.marko";
2export * as layout from "./layout.marko";
2import * as LayoutModule__markodown__ from "./layout.marko";3import * as LayoutModule__markodown__ from "./layout.marko";
3import * as self__markodown__ from "./self.marko";4import * 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 @@
1import Layout__markodown__ from "./layout.marko";1import Layout__markodown__ from "./layout.marko";
2export * as layout from "./layout.marko";
2import * as LayoutModule__markodown__ from "./layout.marko";3import * 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 @@
1import Layout__markodown__ from "./layout.marko";1import Layout__markodown__ from "./layout.marko";
2export * as layout from "./layout.marko";
2import * as LayoutModule__markodown__ from "./layout.marko";3import * 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
2xxx 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>
5xxxxxxxxxx. 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
7xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx.</p>
tests/fixtures/32-inline-tag-in-paragraph.mdo created+9
...@@ -0,0 +1,9 @@
1xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx
2xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx.
3
4xxx xxxxx xx xxx xx xxxx xxxxxxxxxx xxx xxx xxxx xxx xxxxxxxxxx,
5<b>"xxxxxx"</b> xxxxxxxxxx xxx <b>"xxxxxx"</b>
6xxxxxxxxxx. xxxxxx xxxxxxxxxx xxx'x xxxx `xxxxxxxx`, `xxxxxxxxx`.
7
8xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx
9xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx.
wtf.mdo created+9
...@@ -0,0 +1,9 @@
1xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx
2xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx.
3
4xxx xxxxx xx xxx xx xxxx xxxxxxxxxx xxx xxx xxxx xxx xxxxxxxxxx,
5<b>"xxxxxx"</b> xxxxxxxxxx xxx <b>"xxxxxx"</b>
6xxxxxxxxxx. xxxxxx xxxxxxxxxx xxx'x xxxx `xxxxxxxx`, `xxxxxxxxx`.
7
8xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx
9xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx.