From 6eb9bf74cdab5c0e2e930653d43115951d0a711c Mon Sep 17 00:00:00 2001 From: Spenser Black Date: Fri, 8 Nov 2024 09:01:04 -0500 Subject: [PATCH] Implement renderer (#229) --- .../__snapshots__/parser.test.js.snap | 612 ++++++++++++++++++ .../__snapshots__/steamdown.test.js.snap | 505 +++++++++++++++ .../steamdown/__tests__/assets/demo.test.txt | 68 ++ .../steamdown/__tests__/steamdown.test.js | 11 + packages/steamdown/src/index.ts | 1 + packages/steamdown/src/renderer.ts | 96 +++ 6 files changed, 1293 insertions(+) create mode 100644 packages/steamdown/__tests__/__snapshots__/steamdown.test.js.snap create mode 100644 packages/steamdown/__tests__/assets/demo.test.txt create mode 100644 packages/steamdown/__tests__/steamdown.test.js create mode 100644 packages/steamdown/src/renderer.ts diff --git a/packages/steamdown/__tests__/__snapshots__/parser.test.js.snap b/packages/steamdown/__tests__/__snapshots__/parser.test.js.snap index e7b2e68..867e07f 100644 --- a/packages/steamdown/__tests__/__snapshots__/parser.test.js.snap +++ b/packages/steamdown/__tests__/__snapshots__/parser.test.js.snap @@ -200,6 +200,618 @@ exports[`parser parse() .tree complex list 1`] = ` } `; +exports[`parser parse() .tree demo 1`] = ` +{ + "nodes": [ + { + "nodes": [ + { + "text": "This is a paragraph. Breaklines +will be rendered.", + "type": "text", + }, + ], + "type": "paragraph", + }, + { + "nodes": [ + { + "text": "Two breaklines will start a new paragraph.", + "type": "text", + }, + ], + "type": "paragraph", + }, + { + "level": 1, + "nodes": [ + { + "text": "Header Level 1", + "type": "text", + }, + ], + "type": "heading", + }, + { + "level": 2, + "nodes": [ + { + "text": "Header Level 2", + "type": "text", + }, + ], + "type": "heading", + }, + { + "level": 3, + "nodes": [ + { + "text": "Header Level 3", + "type": "text", + }, + ], + "type": "heading", + }, + { + "level": 1, + "nodes": [ + { + "text": "Alternative Header 1", + "type": "text", + }, + ], + "type": "heading", + }, + { + "level": 2, + "nodes": [ + { + "text": "Alternative Header 2", + "type": "text", + }, + ], + "type": "heading", + }, + { + "nodes": [ + { + "text": "Syntax includes:", + "type": "text", + }, + ], + "type": "paragraph", + }, + { + "items": [ + { + "nodes": [ + { + "nodes": [ + { + "nodes": [ + { + "text": "italics", + "type": "text", + }, + ], + "type": "italics", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + { + "nodes": [ + { + "nodes": [ + { + "nodes": [ + { + "text": "bold", + "type": "text", + }, + ], + "type": "bold", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + { + "nodes": [ + { + "nodes": [ + { + "nodes": [ + { + "text": "bold and italics", + "type": "text", + }, + ], + "type": "bold-italics", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + { + "nodes": [ + { + "nodes": [ + { + "nodes": [ + { + "text": "underlines", + "type": "text", + }, + ], + "type": "underline", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + { + "nodes": [ + { + "nodes": [ + { + "nodes": [ + { + "text": "spoilers", + "type": "text", + }, + ], + "type": "spoiler", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + { + "nodes": [ + { + "nodes": [ + { + "nodes": [ + { + "text": "strike outs", + "type": "text", + }, + ], + "type": "strike", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + { + "nodes": [ + { + "nodes": [ + { + "nodes": [ + { + "nodes": [ + { + "text": "you can mix and match BTW", + "type": "text", + }, + ], + "type": "underline", + }, + ], + "type": "italics", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + { + "nodes": [ + { + "nodes": [ + { + "text": "noparse spans are literal. *this won't be rendered* and Steam will show literal [i]", + "type": "noparse-span", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + ], + "ordered": false, + "type": "list", + }, + { + "text": "Noparse can also be used in block form. +This can be useful if you want to demo Steam syntax. +For example, [i]italicizes[/i] text.", + "type": "noparse-block", + }, + { + "text": "code can only be a block. +Because that's how Steam works.", + "type": "code-block", + }, + { + "items": [ + { + "nodes": [ + { + "nodes": [ + { + "text": "List can be ordered", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + { + "nodes": [ + { + "nodes": [ + { + "text": "like this", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + { + "nodes": [ + { + "nodes": [ + { + "text": "Lists can contain complex syntax as long as it's indented properly. For example:", + "type": "text", + }, + ], + "type": "paragraph", + }, + { + "items": [ + { + "nodes": [ + { + "nodes": [ + { + "text": "see how this is aligned with the rest of the text.", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + ], + "ordered": false, + "type": "list", + }, + ], + "type": "list-item", + }, + { + "nodes": [ + { + "nodes": [ + { + "text": "It should always be aligned.", + "type": "text", + }, + ], + "type": "paragraph", + }, + { + "items": [ + { + "nodes": [ + { + "nodes": [ + { + "text": "like this.", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list-item", + }, + ], + "ordered": false, + "type": "list", + }, + ], + "type": "list-item", + }, + ], + "ordered": true, + "type": "list", + }, + { + "author": [ + "author", + undefined, + ], + "nodes": [ + { + "nodes": [ + { + "text": "quotes must have spaces after the ", + "type": "text", + }, + { + "text": "> ", + "type": "noparse-span", + }, + { + "text": " +This is to avoid conflicts with ", + "type": "text", + }, + { + "nodes": [ + { + "text": "spoiler", + "type": "text", + }, + ], + "type": "spoiler", + }, + { + "text": " syntax.", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "quote", + }, + { + "nodes": [ + { + "link": "https://example.com/inline/", + "nodes": [ + { + "text": "urls can be like this", + "type": "text", + }, + ], + "type": "link-url", + }, + { + "text": " +", + "type": "text", + }, + { + "id": "ref", + "nodes": [ + { + "text": "or use a ", + "type": "text", + }, + { + "nodes": [ + { + "text": "reference", + "type": "text", + }, + ], + "type": "italics", + }, + ], + "type": "id-url", + }, + ], + "type": "paragraph", + }, + { + "type": "horizontal-rule", + }, + { + "attributes": { + "equalCells": false, + "noBorder": false, + "type": "table-attribute-row", + }, + "body": [ + { + "cells": [ + { + "nodes": [ + { + "text": "normal", + "type": "text", + }, + ], + "type": "table-cell", + }, + { + "nodes": [ + { + "text": "style", + "type": "text", + }, + ], + "type": "table-cell", + }, + ], + "type": "table-row", + }, + ], + "head": { + "cells": [ + { + "nodes": [ + { + "text": "table", + "type": "text", + }, + ], + "type": "table-cell", + }, + { + "nodes": [ + { + "text": "example", + "type": "text", + }, + ], + "type": "table-cell", + }, + ], + "type": "table-row", + }, + "type": "table", + }, + { + "attributes": { + "equalCells": true, + "noBorder": false, + "type": "table-attribute-row", + }, + "body": [ + { + "cells": [ + { + "nodes": [ + { + "text": "equal", + "type": "text", + }, + ], + "type": "table-cell", + }, + { + "nodes": [ + { + "text": "cells", + "type": "text", + }, + ], + "type": "table-cell", + }, + ], + "type": "table-row", + }, + ], + "head": { + "cells": [ + { + "nodes": [ + { + "text": "table", + "type": "text", + }, + ], + "type": "table-cell", + }, + { + "nodes": [ + { + "text": "example", + "type": "text", + }, + ], + "type": "table-cell", + }, + ], + "type": "table-row", + }, + "type": "table", + }, + { + "attributes": { + "equalCells": false, + "noBorder": true, + "type": "table-attribute-row", + }, + "body": [ + { + "cells": [ + { + "nodes": [ + { + "text": "no", + "type": "text", + }, + ], + "type": "table-cell", + }, + { + "nodes": [ + { + "text": "border", + "type": "text", + }, + ], + "type": "table-cell", + }, + ], + "type": "table-row", + }, + ], + "head": { + "cells": [ + { + "nodes": [ + { + "text": "table", + "type": "text", + }, + ], + "type": "table-cell", + }, + { + "nodes": [ + { + "text": "example", + "type": "text", + }, + ], + "type": "table-cell", + }, + ], + "type": "table-row", + }, + "type": "table", + }, + { + "id": "ref", + "type": "reference", + "url": "https://example.com/ref/", + }, + ], + "type": "root", +} +`; + exports[`parser parse() .tree escaped asterisks 1`] = ` { "nodes": [ diff --git a/packages/steamdown/__tests__/__snapshots__/steamdown.test.js.snap b/packages/steamdown/__tests__/__snapshots__/steamdown.test.js.snap new file mode 100644 index 0000000..aa19f13 --- /dev/null +++ b/packages/steamdown/__tests__/__snapshots__/steamdown.test.js.snap @@ -0,0 +1,505 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`steamdown backslashes in noparse are not escapes 1`] = `"[noparse]\\{no escape\\[/noparse]}"`; + +exports[`steamdown blank lines 1`] = `""`; + +exports[`steamdown blank lines followed by paragraph 1`] = `"Hello, World!"`; + +exports[`steamdown bold 1`] = `"[b]Bold text[/b]"`; + +exports[`steamdown bold and italicized 1`] = `"[b][i]Bold and italicized text[/i][/b]"`; + +exports[`steamdown braces can be used in noparse with space 1`] = `"[noparse]{optional inner space for delimiting}[/noparse]"`; + +exports[`steamdown code block 1`] = ` +"[code] +This is a code block +[/code]" +`; + +exports[`steamdown complex list 1`] = ` +"[list] + [*]item one: + +[list] + [*]sub-item one + [*]sub-item two +[/list] + [*][quote] +a quote in a list +[/quote] +[/list]" +`; + +exports[`steamdown demo 1`] = ` +"This is a paragraph. Breaklines +will be rendered. + +Two breaklines will start a new paragraph. + +[h1]Header Level 1[/h1] + +[h2]Header Level 2[/h2] + +[h3]Header Level 3[/h3] + +[h1]Alternative Header 1[/h1] + +[h2]Alternative Header 2[/h2] + +Syntax includes: + +[list] + [*][i]italics[/i] + [*][b]bold[/b] + [*][b][i]bold and italics[/i][/b] + [*][u]underlines[/u] + [*][spoiler]spoilers[/spoiler] + [*][strike]strike outs[/strike] + [*][i][u]you can mix and match BTW[/u][/i] + [*][noparse]noparse spans are literal. *this won't be rendered* and Steam will show literal [i][/noparse] +[/list] + +[noparse] +Noparse can also be used in block form. +This can be useful if you want to demo Steam syntax. +For example, [i]italicizes[/i] text. +[/noparse] + +[code] +code can only be a block. +Because that's how Steam works. +[/code] + +[olist] + [*]List can be ordered + [*]like this + [*]Lists can contain complex syntax as long as it's indented properly. For example: + +[list] + [*]see how this is aligned with the rest of the text. +[/list] + [*]It should always be aligned. + +[list] + [*]like this. +[/list] +[/olist] + +[quote=author] +quotes must have spaces after the [noparse]> [/noparse] +This is to avoid conflicts with [spoiler]spoiler[/spoiler] syntax. +[/quote] + +[url=https://example.com/inline/]urls can be like this[/url] +[url=https://example.com/ref/]or use a [i]reference[/i][/url] + + +[hr][/hr] + + +[table] + [tr] + [th]table[/th] + [th]example[/th] + [/tr] + [tr] + [td]normal[/td] + [td]style[/td] + [/tr] +[/table] + +[table equalcells=1] + [tr] + [th]table[/th] + [th]example[/th] + [/tr] + [tr] + [td]equal[/td] + [td]cells[/td] + [/tr] +[/table] + +[table noborder=1] + [tr] + [th]table[/th] + [th]example[/th] + [/tr] + [tr] + [td]no[/td] + [td]border[/td] + [/tr] +[/table]" +`; + +exports[`steamdown escaped asterisks 1`] = `"*escaped*"`; + +exports[`steamdown escaped asterisks in bold 1`] = `"[b]bold *escaped*[/b]"`; + +exports[`steamdown escaped asterisks in italics 1`] = `"[i]italics *escaped*[/i]"`; + +exports[`steamdown escaped backslash 1`] = `"Use a \\ to escape special characters"`; + +exports[`steamdown heading 1`] = ` +"[h1]Heading 1[/h1] + +[h2]Heading 2[/h2] + +[h3]Heading 3[/h3] + +[h4]Heading 4[/h4] + +[h5]Heading 5[/h5] + +[h6]Heading 6[/h6]" +`; + +exports[`steamdown heading alternative 1`] = ` +"[h1]Alt heading 1[/h1] + +[h2]Alt heading 2[/h2]" +`; + +exports[`steamdown heading alternative followed by paragraph 1`] = ` +"[h1]Alt heading 1[/h1] + +Paragraph + +[h1]Alt heading 1[/h1] + +Paragraph" +`; + +exports[`steamdown heading alternative with style 1`] = `"[h1][i]styled alt heading 1[/i][/h1]"`; + +exports[`steamdown heading followed by paragraph 1`] = ` +"[h1]Heading[/h1] + +Paragraph + +[h2]Heading[/h2] + +Paragraph" +`; + +exports[`steamdown heading with style 1`] = `"[h1][i]styled heading[/i][/h1]"`; + +exports[`steamdown horizontal rule 1`] = ` +" +[hr][/hr] + + + +[hr][/hr] + + + +[hr][/hr] +" +`; + +exports[`steamdown imbalanced strike leaning left 1`] = `"~~strike~"`; + +exports[`steamdown imbalanced strike leaning right 1`] = `"[strike]strike[/strike]~"`; + +exports[`steamdown invalid italicized text broken into two paragraphs 1`] = ` +"Invalid *italicized + +text* with newlines" +`; + +exports[`steamdown invalid italics with spaces 1`] = `"Invalid * italicized * text with spaces"`; + +exports[`steamdown italicized text 1`] = `"[i]Italicized text[/i]"`; + +exports[`steamdown italics inside word 1`] = `"a[i]b[/i]c"`; + +exports[`steamdown italics plain then more italics 1`] = `"[i]Italics[/i] and [i]more italics[/i]"`; + +exports[`steamdown mixed lists 1`] = ` +"[list] + [*]using an asterisk +[/list] + +[list] + [*]using a dash +[/list] + +[olist] + [*]using a number +[/olist]" +`; + +exports[`steamdown nested italicized text 1`] = `"Nested [i]italicized[/i] text"`; + +exports[`steamdown noparse block 1`] = ` +"[noparse] +not parsed +[/noparse]" +`; + +exports[`steamdown noparse block followed by paragraph 1`] = ` +"[noparse] +noparse +[/noparse] + +paragraph + +[noparse] +noparse +[/noparse] + +paragraph" +`; + +exports[`steamdown noparse block with inner braces 1`] = ` +"[noparse] +{{{ +here's how to noparse +}}} +[/noparse]" +`; + +exports[`steamdown noparse inline 1`] = `"[noparse]not parsed[/noparse]"`; + +exports[`steamdown noparse inline and another noparse inline 1`] = `"[noparse]not parsed[/noparse] [noparse]also not parsed[/noparse]"`; + +exports[`steamdown noparse inline with special contents 1`] = `"[noparse]*not italicized*[/noparse]"`; + +exports[`steamdown noparse inline with two braces 1`] = `"[noparse]not parsed[/noparse]"`; + +exports[`steamdown ordered list 1`] = ` +"[olist] + [*]one + [*]two + [*]three +[/olist]" +`; + +exports[`steamdown ordered list with deep indentation 1`] = ` +"[olist] + [*]a line +that continues. + [*]Two paragraphs... + +One item. + [*]10 uses +extra indentation +[/olist]" +`; + +exports[`steamdown paragraph and another paragraph 1`] = ` +"One paragraph + +Another paragraph" +`; + +exports[`steamdown paragraph spanning lines 1`] = ` +"one +paragraph +spanning lines" +`; + +exports[`steamdown paragraph with extra newlines 1`] = `"paragraph with extra newlines"`; + +exports[`steamdown paragraph without trailing newline 1`] = `"paragraph without trailing newline"`; + +exports[`steamdown paragraphs separated by horizontal rule 1`] = ` +"Text before a horizontal rule + + +[hr][/hr] + + +Text after a horizontal rule" +`; + +exports[`steamdown plain text followed by url 1`] = `"plain text followed by [url=https://example.com]url[/url]."`; + +exports[`steamdown quote 1`] = ` +"[quote] +This is a quote. + +It quotes things. +[/quote]" +`; + +exports[`steamdown quote nested 1`] = ` +"[quote] +You said... + +[quote] +This is a test. +[/quote] + +I said... + +[quote] +Yes it is. +[/quote] +[/quote]" +`; + +exports[`steamdown quote with author 1`] = ` +"[quote=me] +I hope quotes work... +[/quote] + +[quote=me;123] +I hope they work with post IDs, too... +[/quote]" +`; + +exports[`steamdown reference 1`] = `""`; + +exports[`steamdown reference after paragraph 1`] = `"paragraph"`; + +exports[`steamdown reference before paragraph 1`] = `"paragraph"`; + +exports[`steamdown simple 1`] = `"Hello, World!"`; + +exports[`steamdown spoiler 1`] = `"[spoiler]spoiler[/spoiler]"`; + +exports[`steamdown spoiler inside text 1`] = `"This is a [spoiler]spoiler inside text[/spoiler]."`; + +exports[`steamdown strike with one tilde 1`] = `"[strike]strike[/strike]"`; + +exports[`steamdown strike with two tilde 1`] = `"[strike]strike[/strike]"`; + +exports[`steamdown table 1`] = ` +"[table] + [tr] + [th]one[/th] + [th]two[/th] + [/tr] + [tr] + [td]a[/td] + [td]b[/td] + [/tr] +[/table]" +`; + +exports[`steamdown table equalcells 1`] = ` +"[table equalcells=1] + [tr] + [th]one[/th] + [th]two[/th] + [/tr] + [tr] + [td]a[/td] + [td]b[/td] + [/tr] +[/table]" +`; + +exports[`steamdown table equalcells noborder 1`] = ` +"[table noborder=1 equalcells=1] + [tr] + [th]one[/th] + [th]two[/th] + [/tr] + [tr] + [td]a[/td] + [td]b[/td] + [/tr] +[/table]" +`; + +exports[`steamdown table extra head 1`] = ` +"| one | two | +| three | four | +| ----- | ---- |" +`; + +exports[`steamdown table no attributes 1`] = ` +"| one | two | +| a | b |" +`; + +exports[`steamdown table no body 1`] = ` +"[table] + [tr] + [th]one[/th] + [th]two[/th] + [/tr] + +[/table]" +`; + +exports[`steamdown table noborder 1`] = ` +"[table noborder=1] + [tr] + [th]one[/th] + [th]two[/th] + [/tr] + [tr] + [td]a[/td] + [td]b[/td] + [/tr] +[/table]" +`; + +exports[`steamdown table with multiple rows 1`] = ` +"[table] + [tr] + [th]one[/th] + [th]two[/th] + [/tr] + [tr] + [td]a[/td] + [td]b[/td] + [/tr] + [tr] + [td]c[/td] + [td]d[/td] + [/tr] + [tr] + [td]e[/td] + [td]f[/td] + [/tr] +[/table]" +`; + +exports[`steamdown two paragraphs 1`] = ` +"Foo + +Bar" +`; + +exports[`steamdown underlined 1`] = `"[u]Underlined text[/u]"`; + +exports[`steamdown underlined in italicized 1`] = `"[i]Italicized and [u]underlined[/u][/i]"`; + +exports[`steamdown unordered list with asterisks 1`] = ` +"[list] + [*]1 + [*]2 + [*]3 +[/list]" +`; + +exports[`steamdown unordered list with dashes 1`] = ` +"[list] + [*]1 + [*]2 + [*]3 +[/list]" +`; + +exports[`steamdown url link 1`] = `"[url=https://example.com]url with link[/url]"`; + +exports[`steamdown url link with style 1`] = `"[url=https://example.com][i]styled url[/i] with link[/url]"`; + +exports[`steamdown url ref with id 1`] = `"[url=https://example.com/id/]url with id[/url]"`; + +exports[`steamdown url ref with id and style 1`] = `"[url=https://example.com/id/][i]styled url[/i] with id[/url]"`; + +exports[`steamdown url ref without id 1`] = `"[url=https://example.com/plain/]plain-url[/url]"`; + +exports[`steamdown url ref without id with style 1`] = `"[url=https://example.com/styled/][i]styled url[/i][/url]"`; + +exports[`steamdown valid italicized text with a newline 1`] = ` +"Valid [i]italicized +text[/i] with a newline" +`; diff --git a/packages/steamdown/__tests__/assets/demo.test.txt b/packages/steamdown/__tests__/assets/demo.test.txt new file mode 100644 index 0000000..a943000 --- /dev/null +++ b/packages/steamdown/__tests__/assets/demo.test.txt @@ -0,0 +1,68 @@ +This is a paragraph. Breaklines +will be rendered. + +Two breaklines will start a new paragraph. + +# Header Level 1 + +## Header Level 2 + +### Header Level 3 + +Alternative Header 1 +==================== + +Alternative Header 2 +-------------------- + +Syntax includes: + +* *italics* +* **bold** +* ***bold and italics*** +* _underlines_ +* >!spoilers!< +* ~~strike outs~~ +* *_you can mix and match BTW_* +* {noparse spans are literal. *this won't be rendered* and Steam will show literal [i]} + +{{{ +Noparse can also be used in block form. +This can be useful if you want to demo Steam syntax. +For example, [i]italicizes[/i] text. +}}} + +``` +code can only be a block. +Because that's how Steam works. +``` + +1. List can be ordered +2. like this +3. Lists can contain complex syntax as long as it's indented properly. For example: + * see how this is aligned with the rest of the text. +10. It should always be aligned. + * like this. + +> quotes must have spaces after the {> } +> This is to avoid conflicts with >!spoiler!< syntax. +(author) + +[urls can be like this](https://example.com/inline/) +[or use a *reference*][ref] + +***************************** + +| table | example | +| ------ | ------- | +| normal | style | + +| table | example | +| :---: | :-----: | +| equal | cells | + +| table | example | +| | | +| no | border | + +[ref]: https://example.com/ref/ \ No newline at end of file diff --git a/packages/steamdown/__tests__/steamdown.test.js b/packages/steamdown/__tests__/steamdown.test.js new file mode 100644 index 0000000..bbf4e91 --- /dev/null +++ b/packages/steamdown/__tests__/steamdown.test.js @@ -0,0 +1,11 @@ +const { parse, render } = require("../dist"); +const useAssets = require('./assets'); + +describe("steamdown", () => { + const assets = useAssets(); + test.each(assets)("$name", async ({ content }) => { + const { tree, context } = parse(await content); + const rendered = render(tree, context); + expect(rendered).toMatchSnapshot(); + }); +}); diff --git a/packages/steamdown/src/index.ts b/packages/steamdown/src/index.ts index 4fa3c1e..681f61a 100644 --- a/packages/steamdown/src/index.ts +++ b/packages/steamdown/src/index.ts @@ -1,4 +1,5 @@ export * from "./context.js"; export * from "./parser/index.js"; +export * from "./renderer.js"; import type * as nodes from "./nodes"; export { nodes }; diff --git a/packages/steamdown/src/renderer.ts b/packages/steamdown/src/renderer.ts new file mode 100644 index 0000000..08a31af --- /dev/null +++ b/packages/steamdown/src/renderer.ts @@ -0,0 +1,96 @@ +import type { Context } from "./context"; +import type * as nodes from "./nodes"; + +// TODO Performance can be improved by not doing map + filter + join. + +/** + * Renders a block that only has text. + */ +const renderSimpleBlock = (tag: string, content: string): string => `[${tag}]\n${content}\n[/${tag}]`; + +const renderInlineNodes = (nodes: nodes.Inline[], context: Context): string => nodes.map((node) => { + switch (node.type) { + case "text": + return node.text; + case "escaped": + return node.character; + case "bold-italics": + return `[b][i]${renderInlineNodes(node.nodes, context)}[/i][/b]`; + case "bold": + return `[b]${renderInlineNodes(node.nodes, context)}[/b]`; + case "italics": + return `[i]${renderInlineNodes(node.nodes, context)}[/i]`; + case "underline": + return `[u]${renderInlineNodes(node.nodes, context)}[/u]`; + case "spoiler": + return `[spoiler]${renderInlineNodes(node.nodes, context)}[/spoiler]`; + case "strike": + return `[strike]${renderInlineNodes(node.nodes, context)}[/strike]`; + case "noparse-span": + return `[noparse]${node.text}[/noparse]`; + case "link-url": + return `[url=${node.link}]${renderInlineNodes(node.nodes, context)}[/url]`; + case "id-url": { + const link = context.getLink(node.id); + const content = node.nodes != null ? renderInlineNodes(node.nodes, context) : node.id; + // NOTE If the link is not found, we just render the text. + return link != null ? `[url=${link}]${content}[/url]` : `[${content}]`; + } + } +}).join(""); + +const renderList = (list: nodes.List, context: Context): string => { + const tag = list.ordered ? "olist" : "list"; + const items = list.items.map((item) => ` [*]${renderBlocks(item.nodes, context)}`).join("\n"); + return `[${tag}]\n${items}\n[/${tag}]`; +}; + +const renderTableRow = (row: nodes.TableRow, context: Context, cellTag: "td" | "th"): string => { + const renderedCells = row.cells.map((cell) => ` [${cellTag}]${renderInlineNodes(cell.nodes, context)}[/${cellTag}]`).join("\n"); + return ` [tr]\n${renderedCells}\n [/tr]`; +}; + +const renderTable = (table: nodes.Table, context: Context): string => { + let openTag = "table"; + if (table.attributes.noBorder) { + openTag += " noborder=1"; + } + if (table.attributes.equalCells) { + openTag += " equalcells=1"; + } + const renderedHeader = renderTableRow(table.head, context, "th"); + const renderedBody = table.body.map((row) => renderTableRow(row, context, "td")).join("\n"); + return `[${openTag}]\n${renderedHeader}\n${renderedBody}\n[/table]`; +}; + +const renderBlocks = (blocks: nodes.Block[], context: Context): string => blocks.map((node) => { + switch (node.type) { + case "reference": + // NOTE References are not rendered + return null; + case "noparse-block": + return renderSimpleBlock("noparse", node.text); + case "code-block": + return renderSimpleBlock("code", node.text); + case "horizontal-rule": + return "\n[hr][/hr]\n"; + case "heading": + return `[h${node.level}]${renderInlineNodes(node.nodes, context)}[/h${node.level}]`; + case "quote": { + const [author, postId] = node.author ?? []; + const openTag = author != null ? `[quote=${author}${postId != null ? `;${postId}` : ""}]` : "[quote]"; + return `${openTag}\n${renderBlocks(node.nodes, context)}\n[/quote]`; + } + case "paragraph": + return renderInlineNodes(node.nodes, context); + case "list": + return renderList(node, context); + case "table": + return renderTable(node, context); + } +}).filter((rendered) => rendered != null).join("\n\n"); + +/** + * Renders the syntax tree into Steam's markup. + */ +export const render = (root: nodes.Root, context: Context): string => renderBlocks(root.nodes, context);