From 1eea52db1f3dc2d45b751918acb6fcc03fe3bb4b Mon Sep 17 00:00:00 2001 From: Zac Rosenbauer Date: Thu, 12 Dec 2024 15:12:17 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=BE=20saving=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/index.ts | 12 +- examples/{lib => inputs}/basic.ts | 2 +- examples/{lib => inputs}/code.ts | 2 +- examples/inputs/frontmatter.ts | 31 +++++ examples/{lib => inputs}/table.ts | 2 +- examples/{docs => outputs}/basic.md | 0 examples/{docs => outputs}/code.md | 0 examples/outputs/frontmatter.md | 37 +++++ examples/{docs => outputs}/table.md | 0 package.json | 1 + packages/tempo/package.json | 4 +- .../src/lib/__tests__/tempo-document.test.ts | 131 ++++++++++++++---- .../markdown/__tests__/frontmatter.test.ts | 11 ++ .../tempo/src/lib/markdown/frontmatter.ts | 16 +++ packages/tempo/src/lib/markdown/markdown.ts | 1 + packages/tempo/src/lib/tempo-document.ts | 54 +++++++- yarn.lock | 10 ++ 17 files changed, 275 insertions(+), 39 deletions(-) rename examples/{lib => inputs}/basic.ts (93%) rename examples/{lib => inputs}/code.ts (91%) create mode 100644 examples/inputs/frontmatter.ts rename examples/{lib => inputs}/table.ts (87%) rename examples/{docs => outputs}/basic.md (100%) rename examples/{docs => outputs}/code.md (100%) create mode 100644 examples/outputs/frontmatter.md rename examples/{docs => outputs}/table.md (100%) create mode 100644 packages/tempo/src/lib/markdown/__tests__/frontmatter.test.ts create mode 100644 packages/tempo/src/lib/markdown/frontmatter.ts diff --git a/examples/index.ts b/examples/index.ts index 1984bbc..4341e60 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -3,9 +3,10 @@ import path from 'node:path'; const __dirname = path.dirname(new URL(import.meta.url).pathname); -import exampleBasic from './lib/basic'; -import exampleCode from './lib/code'; -import exampleTable from './lib/table'; +import exampleBasic from './inputs/basic'; +import exampleCode from './inputs/code'; +import exampleFrontmatter from './inputs/frontmatter'; +import exampleTable from './inputs/table'; /* |---------------------------------- @@ -25,16 +26,19 @@ import exampleTable from './lib/table'; const basicResult = exampleBasic(); const codeResult = exampleCode(); const tableResult = exampleTable(); + const frontmatterResult = exampleFrontmatter(); - const examplesDir = path.join(__dirname, '..', 'examples', 'docs'); + const examplesDir = path.join(__dirname, '..', 'examples', 'outputs'); const basicFile = path.join(examplesDir, 'basic.md'); const codeFile = path.join(examplesDir, 'code.md'); const tableFile = path.join(examplesDir, 'table.md'); + const frontmatterFile = path.join(examplesDir, 'frontmatter.md'); Promise.all([ fs.writeFile(basicFile, basicResult), fs.writeFile(codeFile, codeResult), fs.writeFile(tableFile, tableResult), + fs.writeFile(frontmatterFile, frontmatterResult), ]); console.log('Examples built successfully.'); diff --git a/examples/lib/basic.ts b/examples/inputs/basic.ts similarity index 93% rename from examples/lib/basic.ts rename to examples/inputs/basic.ts index aa8e89b..bfb083f 100644 --- a/examples/lib/basic.ts +++ b/examples/inputs/basic.ts @@ -1,4 +1,4 @@ -import tempo from '../../src'; +import tempo from '../../packages/tempo/src'; function run() { return tempo() diff --git a/examples/lib/code.ts b/examples/inputs/code.ts similarity index 91% rename from examples/lib/code.ts rename to examples/inputs/code.ts index 302f416..356272d 100644 --- a/examples/lib/code.ts +++ b/examples/inputs/code.ts @@ -1,4 +1,4 @@ -import tempo from '../../src'; +import tempo from '../../packages/tempo/src'; function run() { return tempo() diff --git a/examples/inputs/frontmatter.ts b/examples/inputs/frontmatter.ts new file mode 100644 index 0000000..7cc3856 --- /dev/null +++ b/examples/inputs/frontmatter.ts @@ -0,0 +1,31 @@ +import tempo from '../../packages/tempo/src'; + +function run() { + return tempo() + .frontmatter({ + title: 'Frontmatter Example', + description: 'This is an example of frontmatter', + tags: ['example', 'frontmatter'], + published: true, + version: 1, + }) + .h1('Hello World') + .paragraph('This is a paragraph') + .h2((text) => text.plainText('This is a heading with ').bold('bold text')) + .paragraph((text) => text.bold('This is bold text')) + .paragraph( + (text) => `This is inline text ${text.italic('with italic text')}` + ) + .paragraph((text) => + text.plainText('Foobar is a thing').bold('that is bold') + ) + .h2('Lists') + .alert('This is an alert', 'caution') + .paragraph('This is a list') + .bulletList(['Item 1', 'Item 2', 'Item 3']) + .paragraph('This is a numbered list') + .numberList(['Item 1', 'Item 2', 'Item 3']) + .toString(); +} + +export default run; diff --git a/examples/lib/table.ts b/examples/inputs/table.ts similarity index 87% rename from examples/lib/table.ts rename to examples/inputs/table.ts index 8645253..ed2ab1b 100644 --- a/examples/lib/table.ts +++ b/examples/inputs/table.ts @@ -1,4 +1,4 @@ -import tempo from '../../src'; +import tempo from '../../packages/tempo/src'; function run() { return tempo() diff --git a/examples/docs/basic.md b/examples/outputs/basic.md similarity index 100% rename from examples/docs/basic.md rename to examples/outputs/basic.md diff --git a/examples/docs/code.md b/examples/outputs/code.md similarity index 100% rename from examples/docs/code.md rename to examples/outputs/code.md diff --git a/examples/outputs/frontmatter.md b/examples/outputs/frontmatter.md new file mode 100644 index 0000000..ce8c169 --- /dev/null +++ b/examples/outputs/frontmatter.md @@ -0,0 +1,37 @@ +--- +title: Frontmatter Example +description: This is an example of frontmatter +tags: + - example + - frontmatter +published: true +version: 1 +--- +# Hello World + +This is a paragraph + +## This is a heading with **bold text** + +**This is bold text** + +This is inline text _with italic text_ + +Foobar is a thing **that is bold** + +## Lists + +> [!CAUTION] +> This is an alert + +This is a list + +- Item 1 +- Item 2 +- Item 3 + +This is a numbered list + +1. Item 1 +2. Item 2 +3. Item 3 diff --git a/examples/docs/table.md b/examples/outputs/table.md similarity index 100% rename from examples/docs/table.md rename to examples/outputs/table.md diff --git a/package.json b/package.json index df927d2..7334205 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "packages/*" ], "scripts": { + "generate:examples": "tsx examples/index.ts", "analyze": "biome check", "analyze:types": "turbo analyze:types", "analyze:ci": "biome ci --diagnostic-level=error", diff --git a/packages/tempo/package.json b/packages/tempo/package.json index ed3e50e..24adf2a 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -31,7 +31,6 @@ "dev": "rslib build --watch", "build": "rslib build", "test": "vitest --coverage", - "generate:examples": "tsx examples/index.ts", "analyze:types": "tsc --noEmit" }, "devDependencies": { @@ -43,5 +42,8 @@ "type-fest": "^4.30.0", "typescript": "^5.7.2", "vitest": "^2.1.8" + }, + "dependencies": { + "yaml": "^2.6.1" } } diff --git a/packages/tempo/src/lib/__tests__/tempo-document.test.ts b/packages/tempo/src/lib/__tests__/tempo-document.test.ts index df65470..191cf4e 100644 --- a/packages/tempo/src/lib/__tests__/tempo-document.test.ts +++ b/packages/tempo/src/lib/__tests__/tempo-document.test.ts @@ -1,4 +1,4 @@ -import { describe , it, expect } from 'vitest'; +import { describe , it, expect, beforeEach } from 'vitest'; import { TempoDocument, type TempoDocumentNode } from '../tempo-document'; @@ -34,12 +34,12 @@ describe('initialization', () => { }, ]; const document = new TempoDocument(initJSON); - expect(document.toJSON()).toEqual(initJSON); + expect(document.toJSON().nodes).toEqual(initJSON); }); it('should return empty nodes if no input', () => { const document = new TempoDocument(); - expect(document.toJSON()).toEqual([]); + expect(document.toJSON().nodes).toEqual([]); }); }); @@ -65,39 +65,39 @@ describe('Headings', () => { it('should add a h1 heading', () => { const document = new TempoDocument().h1('Hello World!'); - expect(document.toJSON()).toEqual(getResult(1, 'Hello World!')); + expect(document.toJSON().nodes).toEqual(getResult(1, 'Hello World!')); }); it('should add a h2 heading', () => { const document = new TempoDocument().h2('Hello World!'); - expect(document.toJSON()).toEqual(getResult(2, 'Hello World!')); + expect(document.toJSON().nodes).toEqual(getResult(2, 'Hello World!')); }); it('should add a h3 heading', () => { const document = new TempoDocument().h3('Hello World!'); - expect(document.toJSON()).toEqual(getResult(3, 'Hello World!')); + expect(document.toJSON().nodes).toEqual(getResult(3, 'Hello World!')); }); it('should add a h4 heading', () => { const document = new TempoDocument().h4('Hello World!'); - expect(document.toJSON()).toEqual(getResult(4, 'Hello World!')); + expect(document.toJSON().nodes).toEqual(getResult(4, 'Hello World!')); }); it('should add a h5 heading', () => { const document = new TempoDocument().h5('Hello World!'); - expect(document.toJSON()).toEqual(getResult(5, 'Hello World!')); + expect(document.toJSON().nodes).toEqual(getResult(5, 'Hello World!')); }); it('should add a h6 heading', () => { const document = new TempoDocument().h6('Hello World!'); - expect(document.toJSON()).toEqual(getResult(6, 'Hello World!')); + expect(document.toJSON().nodes).toEqual(getResult(6, 'Hello World!')); }); }); describe('Text Elements', () => { it('should add a paragraph', () => { const document = new TempoDocument().paragraph('Hello World!'); - expect(document.toJSON()).toEqual([ + expect(document.toJSON().nodes).toEqual([ { type: 'paragraph', data: { @@ -121,7 +121,7 @@ describe('Special Elements', () => { ['Hello World!', 'Hello 2 World!'], ['Hello 3 World!', 'Hello 4 World!'], ]); - expect(document.toJSON()).toEqual([ + expect(document.toJSON().nodes).toEqual([ { type: 'table', data: { @@ -188,7 +188,7 @@ describe('Special Elements', () => { it('should add html', () => { const document = new TempoDocument().html('
Hello World!
'); - expect(document.toJSON()).toEqual([ + expect(document.toJSON().nodes).toEqual([ { type: 'html', data: { @@ -210,7 +210,7 @@ describe('Special Elements', () => { 'console.log("Hello World!");', 'javascript' ); - expect(document.toJSON()).toEqual([ + expect(document.toJSON().nodes).toEqual([ { type: 'codeBlock', data: { @@ -233,7 +233,7 @@ describe('Special Elements', () => { it('should add a blockquote', () => { const document = new TempoDocument().blockQuote('Hello World!'); - expect(document.toJSON()).toEqual([ + expect(document.toJSON().nodes).toEqual([ { type: 'blockQuote', data: { @@ -256,7 +256,7 @@ describe('Special Elements', () => { 'https://example.com/image.png' ); - expect(document.toJSON()).toEqual([ + expect(document.toJSON().nodes).toEqual([ { type: 'image', data: { @@ -271,7 +271,7 @@ describe('Special Elements', () => { it('should add a horizontal rule (break)', () => { const document = new TempoDocument().break(); - expect(document.toJSON()).toEqual([ + expect(document.toJSON().nodes).toEqual([ { type: 'break', data: { @@ -294,7 +294,7 @@ describe('Special Elements', () => { ).forEach(({ type, expected }) => { it(`should return a ${expected} alert`, () => { const document = new TempoDocument().alert('Hello World!', type); - expect(document.toJSON()).toEqual([ + expect(document.toJSON().nodes).toEqual([ { type: 'alert', data: { @@ -321,7 +321,7 @@ describe('Lists', () => { 'Hello 2 World!', (txt) => txt.bold('Hello 3 World!'), ]); - expect(document.toJSON()).toEqual([ + expect(document.toJSON().nodes).toEqual([ { type: 'bulletList', data: { @@ -388,7 +388,7 @@ describe('Lists', () => { 'Hello World!', 'Hello 2 World!', ]); - expect(document.toJSON()).toEqual([ + expect(document.toJSON().nodes).toEqual([ { type: 'numberList', data: { @@ -431,12 +431,37 @@ describe('Lists', () => { describe('Outputs', () => { describe('toString', () => { - it('should return a string', () => { - const document = new TempoDocument() + let doc; + + beforeEach(() => { + doc = new TempoDocument() .h1('Hello World!') .paragraph('Hello there!'); - expect(document.toString()).toEqual( + }); + + it('should return a string without frontmatter', () => { + expect(doc.toString()).toEqual( + ` +# Hello World! + +Hello there! + ` + .trim() + .concat('\n') + ); + }); + + it('should return a string with frontmatter', () => { + doc.frontmatter({ + title: 'Hello World!', + foobar: 'Hello there!', + }); + expect(doc.toString()).toEqual( ` +--- +title: Hello World! +foobar: Hello there! +--- # Hello World! Hello there! @@ -448,11 +473,25 @@ Hello there! }); describe('toJSON', () => { - const document = new TempoDocument() - .h1('Hello World!') - .paragraph('Hello there!'); - it('should return a JSON object', () => { - expect(document.toJSON()).toEqual([ + let doc; + + beforeEach(() => { + doc = new TempoDocument() + .h1('Hello World!') + .paragraph('Hello there!'); + }); + + it('should return a JSON object with metadata', () => { + doc.frontmatter({ + title: 'Hello World!', + foobar: 'Hello there!', + }); + expect(doc.toJSON()).toEqual({ + metadata: { + title: 'Hello World!', + foobar: 'Hello there!', + }, + nodes: [ { type: 'heading', data: { @@ -480,7 +519,43 @@ Hello there! }, computed: 'Hello there!', }, - ]); + ] + }); + }); + + it('should return a JSON object without metadata', () => { + expect(doc.toJSON()).toEqual({ + metadata: null, + nodes: [ + { + type: 'heading', + data: { + level: 1, + nodes: [ + { + type: 'plaintext', + data: undefined, + computed: 'Hello World!', + }, + ], + }, + computed: '# Hello World!', + }, + { + type: 'paragraph', + data: { + nodes: [ + { + type: 'plaintext', + data: undefined, + computed: 'Hello there!', + }, + ], + }, + computed: 'Hello there!', + }, + ] + }); }); }); }); diff --git a/packages/tempo/src/lib/markdown/__tests__/frontmatter.test.ts b/packages/tempo/src/lib/markdown/__tests__/frontmatter.test.ts new file mode 100644 index 0000000..52ba30a --- /dev/null +++ b/packages/tempo/src/lib/markdown/__tests__/frontmatter.test.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'vitest'; +import { frontmatter } from '../frontmatter'; + +describe('frontmatter', () => { + it('should convert a JSON object to a YAML frontmatter string', () => { + const value = { title: 'Hello, World!' }; + const result = frontmatter(value); + + expect(result).toBe('---\ntitle: Hello, World!\n---'); + }); +}); \ No newline at end of file diff --git a/packages/tempo/src/lib/markdown/frontmatter.ts b/packages/tempo/src/lib/markdown/frontmatter.ts new file mode 100644 index 0000000..1887c1f --- /dev/null +++ b/packages/tempo/src/lib/markdown/frontmatter.ts @@ -0,0 +1,16 @@ +import type { JsonObject } from 'type-fest'; +import YAML from 'yaml'; + +export type Frontmatter = JsonObject; + +/** + * Converts a JSON/JS/TS object to a YAML frontmatter string. + * + * @param value - JSON object to convert. + * @returns YAML frontmatter string. + */ +export function frontmatter(value: T): string { + const yaml = YAML.stringify(value); + + return ['---', yaml.trim(), '---'].join('\n'); +} diff --git a/packages/tempo/src/lib/markdown/markdown.ts b/packages/tempo/src/lib/markdown/markdown.ts index b9f41c3..c02c268 100644 --- a/packages/tempo/src/lib/markdown/markdown.ts +++ b/packages/tempo/src/lib/markdown/markdown.ts @@ -13,6 +13,7 @@ import * as emo from './emoji'; export * from './code-block'; export * from './emoji'; +export * from './frontmatter'; /* |------------------ diff --git a/packages/tempo/src/lib/tempo-document.ts b/packages/tempo/src/lib/tempo-document.ts index 03b95fd..17b1fe2 100644 --- a/packages/tempo/src/lib/tempo-document.ts +++ b/packages/tempo/src/lib/tempo-document.ts @@ -146,6 +146,20 @@ export type TempoDocumentNode = | BulletListNode | AlertNode; +export type TempoDocumentMetadata = md.Frontmatter | null; + +export interface TempoDocumentJSON { + /** + * The nodes of the document. + */ + nodes: TempoDocumentNode[]; + + /** + * The metadata (frontmatter) of the document. + */ + metadata: TempoDocumentMetadata; +} + /* |---------------------------------- | TempoDocument Class @@ -164,10 +178,35 @@ export type TempoDocumentNode = export class TempoDocument { private readonly nodes: TempoDocumentNode[]; + private metadata: TempoDocumentMetadata = null; + constructor(documentNodes?: TempoDocumentNode[]) { this.nodes = documentNodes ?? []; } + /** + * Add frontmatter to the document. + * + * @example + * ```ts + * const doc = tempo() + * .frontmatter({ title: 'Hello, World!' }) + * .h1('Hello, World!') + * .toString(); + * // Output: + * // --- + * // title: Hello, World! + * // --- + * // # Hello, World! + * + * @param value A JSON object to convert to YAML frontmatter. + * @returns The TempoDocument instance with the frontmatter appended to the document. + */ + public frontmatter(value: TempoDocumentMetadata): this { + this.metadata = value; + return this; + } + /* |------------------ | Headings @@ -697,11 +736,17 @@ export class TempoDocument { * @returns A string representation of the document, that can be used for rendering. */ public toString(): string { - return this.nodes + const result = this.nodes .map((section) => section.computed) .join('\n\n') .trim() .concat('\n'); + + if (this.metadata) { + return md.frontmatter(this.metadata).concat('\n', result); + } + + return result; } /** @@ -742,7 +787,10 @@ export class TempoDocument { * * @returns A JSON representation of the document, that can be used for serialization. */ - public toJSON(): TempoDocumentNode[] { - return this.nodes; + public toJSON(): TempoDocumentJSON { + return { + nodes: this.nodes, + metadata: this.metadata, + }; } } diff --git a/yarn.lock b/yarn.lock index 7fc8ec7..37df0d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -645,6 +645,7 @@ __metadata: type-fest: "npm:^4.30.0" typescript: "npm:^5.7.2" vitest: "npm:^2.1.8" + yaml: "npm:^2.6.1" languageName: unknown linkType: soft @@ -2936,3 +2937,12 @@ __metadata: checksum: 10/1884d272d485845ad04759a255c71775db0fac56308764b4c77ea56a20d56679fad340213054c8c9c9c26fcfd4c4b2a90df993b7e0aaf3cdb73c618d1d1a802a languageName: node linkType: hard + +"yaml@npm:^2.6.1": + version: 2.6.1 + resolution: "yaml@npm:2.6.1" + bin: + yaml: bin.mjs + checksum: 10/cf412f03a33886db0a3aac70bb4165588f4c5b3c6f8fc91520b71491e5537800b6c2c73ed52015617f6e191eb4644c73c92973960a1999779c62a200ee4c231d + languageName: node + linkType: hard