diff --git a/src/components/Code.astro b/src/components/Code.astro new file mode 100644 index 0000000..6a25d38 --- /dev/null +++ b/src/components/Code.astro @@ -0,0 +1,7 @@ +--- +import { leftTrimDedent } from "./utils"; + +const { thpcode } = Astro.props; +--- + +
{leftTrimDedent(thpcode).join("\n")}
diff --git a/src/components/utils.test.ts b/src/components/utils.test.ts new file mode 100644 index 0000000..3a46661 --- /dev/null +++ b/src/components/utils.test.ts @@ -0,0 +1,133 @@ +import { expect, test } from 'vitest' +import { leftTrimDedent } from "./utils" + +test("should trim empty string", () => { + const input = ``; + + expect(leftTrimDedent(input)).toEqual([""]); +}) + +test("should work on a single line", () => { + const input = `hello` + + expect(leftTrimDedent(input)).toEqual([ + "hello" + ]); +}) + +test("should trim a single line", () => { + const input = ` hello` + + expect(leftTrimDedent(input)).toEqual([ + "hello" + ]); +}) + +test("should trim multiple lines", () => { + const input = ` hello\n world` + + expect(leftTrimDedent(input)).toEqual([ + "hello", + "world" + ]); +}) + +test("should trim multiple lines without indentation", () => { + const input = `hello\nworld` + + expect(leftTrimDedent(input)).toEqual([ + "hello", + "world" + ]); +}) + +test("should consume only whitespace", () => { + const input = ` hello\nworld`; + + try { + const res = leftTrimDedent(input); + expect(res).not.toEqual([ + "hello", + "rld", + ]); + } catch (e) { + expect(e).toBeInstanceOf(Error); + expect(e).toHaveProperty("message", "Invalid indentation while trimming: Expected 2 spaces, got 0"); + } +}) + +test("should preserve deeper indentation", () => { + const input = ` hello\n world` + + expect(leftTrimDedent(input)).toEqual([ + "hello", + " world", + ]); +}) + +test("should ignore empty lines", () => { + const input = ` hello\n\n world` + + expect(leftTrimDedent(input)).toEqual([ + "hello", + "", + "world", + ]); +}) + +test("should ignore lines with only whitespace", () => { + const input = ` hello\n \n \n world` + + expect(leftTrimDedent(input)).toEqual([ + "hello", + "", + " ", + "world", + ]); +}) + +test("should trim multiple without indentation", () => { + const input = `hello\nworld\n!` + + expect(leftTrimDedent(input)).toEqual([ + "hello", + "world", + "!", + ]); +}) + +test("should ignore empty first line", () => { + const input = ` +hello +world`; + + expect(leftTrimDedent(input)).toEqual([ + "hello", + "world", + ]); +}) + +test("should ignore empty first line 2", () => { + const input = ` + hello + + world`; + + expect(leftTrimDedent(input)).toEqual([ + "hello", + "", + "world", + ]); +}) + +test("should ignore empty last line", () => { + const input = ` + hello + world +`; + + expect(leftTrimDedent(input)).toEqual([ + "hello", + "world", + ]); +}); diff --git a/src/components/utils.ts b/src/components/utils.ts index d7548f7..09402c0 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -45,7 +45,7 @@ export function trimAndDedent(input: string): Array { let currentIndentation = 0; for (const c of characters) { if (c === " ") { currentIndentation += 1 } - else {break;} + else { break; } } if (currentIndentation < indentationLevel) { throw new Error("Invalid indentation while parsing THP code: " + lines[i]); @@ -67,4 +67,58 @@ export function trimAndDedent(input: string): Array { } return lines; -} \ No newline at end of file +} + +export function leftTrimDedent(input: string): Array { + let lines = input.split("\n"); + let output: Array = []; + + // Ignore first line + if (lines[0] === "" && lines.length > 1) { + lines = lines.slice(1); + } + + // Get indentation level of the first line + let indentationLevel = 0; + for (const char of lines[0]!) { + if (char === " ") { + indentationLevel += 1; + } else { + break; + } + } + + for (const line of lines) { + // Ignore empty lines + if (line === "") { + output.push(""); + continue; + } + output.push(trimWhitespace(line, indentationLevel)); + } + + if (output.length > 1 && output[output.length - 1] === "") { + output = output.slice(0, -1); + } + + return output; +} + +function trimWhitespace(input: string, count: number): string { + let indentCount = 0; + + for (const char of input) { + if (char === " ") { + indentCount += 1; + } else { + break; + } + } + + if (indentCount >= count || indentCount == input.length) { + return input.slice(count); + } else { + throw new Error(`Invalid indentation while trimming: Expected ${count} spaces, got ${indentCount}`); + } +} + diff --git a/src/pages/learn/templating/intro.md b/src/pages/learn/templating/intro.mdx similarity index 97% rename from src/pages/learn/templating/intro.md rename to src/pages/learn/templating/intro.mdx index 500614e..0bfe4b5 100644 --- a/src/pages/learn/templating/intro.md +++ b/src/pages/learn/templating/intro.mdx @@ -2,6 +2,7 @@ layout: ../../../layouts/DocsLayout.astro title: Introduction --- +import Code from "../../../components/Code.astro" # THP templating @@ -71,7 +72,8 @@ fun Button(String name) -> Html { It is very similar to React. The HTML is inside the THP code, not the other way around, so you can have arbitrary logic in the component. -```thp + + } -``` +`} />