diff --git a/package.json b/package.json index 218e6ac..61f54f8 100644 --- a/package.json +++ b/package.json @@ -58,8 +58,10 @@ "lucide-react": "^0.314.0", "luxon": "^2.4.0", "mdast-util-from-markdown": "^2.0.2", + "mdast-util-frontmatter": "^2.0.1", "mdast-util-gfm": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", + "micromark-extension-frontmatter": "^2.0.0", "micromark-extension-gfm": "^3.0.0", "mobx": "^5.15.4", "mobx-react-lite": "^2.0.7", diff --git a/src/electron/migrations/20211005142122.sql b/src/electron/migrations/20211005142122.sql index 22fa2a6..1bf13bb 100644 --- a/src/electron/migrations/20211005142122.sql +++ b/src/electron/migrations/20211005142122.sql @@ -26,6 +26,7 @@ CREATE TABLE IF NOT EXISTS "documents" ( "title" TEXT, "content" TEXT NOT NULL, "journal" TEXT NOT NULL, + "frontmatter" TEXT, FOREIGN KEY ("journal") REFERENCES "journals" ("name") ON DELETE CASCADE ON UPDATE CASCADE ); diff --git a/src/markdown/index.test.ts b/src/markdown/index.test.ts index 995a77b..d3f26ad 100644 --- a/src/markdown/index.test.ts +++ b/src/markdown/index.test.ts @@ -2,6 +2,7 @@ import { expect } from "chai"; import fs from "fs"; import { describe, it } from "mocha"; import path from "path"; +import yaml from "yaml"; import { slateToString, stringToSlate } from "./index.js"; import { dig, parseMarkdown, parseMarkdownForImport } from "./test-utils.js"; @@ -616,3 +617,39 @@ describe("Whacky shit", function () { ****[5 variations of Binary search (A Self Note)](https://leetcode.com/discuss/interview-question/1322500/5-variations-of-Binary-search-(A-Self-Note))****`; }); + +describe.only("front matter parsing", function () { + const content = `--- +title: 2024-09-29 +tags: weekly-persona +createdAt: 2024-09-30T17:50:22.000Z +updatedAt: 2024-11-04T16:24:11.000Z +--- + +\#weekly-persona + +Last week: [2024-09-22](../persona/0193acd4fa3574698c36c4514b907c70.md) + +**I am on call this week** [On call week of 2024-09-30](../persona/0193acd4fa45731f81350d4443c1ed16.md) + +## Monday + +`; + + // it.skip("should parse front matter", function () { + // const parsed = parseMarkdown(content); + // console.log(yaml.parse(parsed.children[0].value as string)); + // }); + + it.only("test how to splice it back in", function () { + const parsed = parseMarkdown(content); + const frontMatter = yaml.parse(parsed.children[0].value as string); + const newFrontMatter = yaml.stringify({ + ...frontMatter, + title: "2024-09-29", + }); + + // ok, it needs --- added + console.log(newFrontMatter); + }); +}); diff --git a/src/markdown/index.ts b/src/markdown/index.ts index a549e43..02a5887 100644 --- a/src/markdown/index.ts +++ b/src/markdown/index.ts @@ -5,8 +5,13 @@ import * as mdast from "mdast"; export { slateToMdast } from "./remark-slate-transformer/transformers/slate-to-mdast.js"; import { fromMarkdown } from "mdast-util-from-markdown"; +import { + frontmatterFromMarkdown, + frontmatterToMarkdown, +} from "mdast-util-frontmatter"; import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm"; import { toMarkdown } from "mdast-util-to-markdown"; +import { frontmatter } from "micromark-extension-frontmatter"; import { gfm } from "micromark-extension-gfm"; import { ofmTagFromMarkdown } from "./mdast-util-ofm-tag"; import { ofmWikilinkFromMarkdown } from "./mdast-util-ofm-wikilink"; @@ -53,25 +58,28 @@ function wrapImages(tree: mdast.Root) { // to Chronicles tags and markdown links. Future versions may support these properly. export const parseMarkdownForImport = (markdown: string): mdast.Root => { return fromMarkdown(markdown, { - extensions: [gfm(), ofmTag(), ofmWikilink()], + extensions: [gfm(), ofmTag(), ofmWikilink(), frontmatter(["yaml"])], mdastExtensions: [ gfmFromMarkdown(), ofmTagFromMarkdown(), ofmWikilinkFromMarkdown(), + // https://github.com/micromark/micromark-extension-frontmatter?tab=readme-ov-file#preset + // todo: support toml (need toml parser) + frontmatterFromMarkdown(["yaml"]), ], }); }; export const parseMarkdown = (markdown: string): mdast.Root => { return fromMarkdown(markdown, { - extensions: [gfm()], - mdastExtensions: [gfmFromMarkdown()], + extensions: [gfm(), frontmatter(["yaml"])], + mdastExtensions: [gfmFromMarkdown(), frontmatterFromMarkdown(["yaml"])], }); }; export const mdastToString = (tree: mdast.Nodes) => { return toMarkdown(tree, { - extensions: [gfmToMarkdown() as any], + extensions: [gfmToMarkdown() as any, frontmatterToMarkdown(["yaml"])], bullet: "-", emphasis: "_", }); diff --git a/src/preload/client/documents.ts b/src/preload/client/documents.ts index 766521d..c139e16 100644 --- a/src/preload/client/documents.ts +++ b/src/preload/client/documents.ts @@ -2,6 +2,7 @@ import { Database } from "better-sqlite3"; import { Knex } from "knex"; import path from "path"; import { uuidv7obj } from "uuidv7"; +import yaml from "yaml"; import { mdastToString, parseMarkdown, selectNoteLinks } from "../../markdown"; import { parseNoteLink } from "../../views/edit/editor/features/note-linking/toMdast"; import { Files } from "../files"; @@ -11,6 +12,7 @@ import { IPreferencesClient } from "./preferences"; import { GetDocumentResponse, + IndexRequest, SaveRequest, SearchItem, SearchRequest, @@ -36,13 +38,19 @@ export class DocumentsClient { ) {} findById = async ({ id }: { id: string }): Promise => { - const document = this.db - .prepare(`SELECT * FROM documents WHERE id = :id`) - .get({ id }); - const documentTags = this.db - .prepare(`SELECT tag FROM document_tags WHERE documentId = :documentId`) - .all({ documentId: id }) - .map((row) => row.tag); + const document = await this.knex("documents").where({ id }).first(); + + // todo: test 404 behavior + if (!document) { + throw new Error(`Document ${id} not found`); + } + + // todo: confirm this pulls out tags correctly post knex change + // todo: should this be dropped, and we just pull tags from the content? + // tags table is then only used for search as a cache, similar to content and title + const documentTags = await this.knex("document_tags") + .where({ documentId: id }) + .select("tag"); const filepath = path.join( await this.preferences.get("NOTES_DIR"), @@ -53,10 +61,11 @@ export class DocumentsClient { // freshly load the document from disk to avoid desync issues // todo: a real strategy for keeping db in sync w/ filesystem, that allows // loading from db. - const { contents } = await this.loadDoc(filepath); + const { contents, frontMatter } = await this.loadDoc(filepath); return { ...document, + frontMatter, contents, tags: documentTags, }; @@ -75,7 +84,7 @@ export class DocumentsClient { del = async (id: string, journal: string) => { await this.files.deleteDocument(id, journal); - this.db.prepare("delete from documents where id = :id").run({ id }); + await this.knex("documents").where({ id }).del(); }; search = async (q?: SearchRequest): Promise => { @@ -154,25 +163,29 @@ export class DocumentsClient { id = args.id; } else { args.createdAt = new Date().toISOString(); - args.updatedAt = new Date().toISOString(); [id] = await this.createDocument(args); } return this.findById({ id }); }; - /** - * Convert the properties we track to frontmatter - */ - contentsWithFrontMatter = (document: SaveRequest) => { - const fm = `--- -title: ${document.title} -tags: ${document.tags.join(", ")} -createdAt: ${document.createdAt} -updatedAt: ${document.updatedAt} ----`; - - return `${fm}\n\n${document.content}`; + // Extend front-matter (if any) with Chronicles standard properties, then + // add to serialized document contents. + prependFrontMatter = (contents: string, frontMatter: Record) => { + // need to re-add ---, and also double-newline the ending frontmatter + const fm = ["---", yaml.stringify(frontMatter), "---"].join("\n"); + + return `${fm}\n\n${contents}`; + }; + + mergedFrontMatter = (document: SaveRequest): Record => { + return { + ...(document.frontMatter || {}), + title: document.title, + tags: document.tags, + createdAt: document.createdAt, + updatedAt: document.updatedAt, + }; }; /** @@ -185,21 +198,37 @@ updatedAt: ${document.updatedAt} index: boolean = true, ): Promise<[string, string]> => { const id = args.id || uuidv7obj().toHex(); - const content = this.contentsWithFrontMatter(args); + const frontMatter = this.mergedFrontMatter(args); + frontMatter.createdAt = frontMatter.createdAt || new Date().toISOString(); + frontMatter.updatedAt = frontMatter.updatedAt || new Date().toISOString(); + + const content = this.prependFrontMatter(args.content, frontMatter); const docPath = await this.files.uploadDocument( { id, content }, args.journal, ); if (index) { - return [await this.createIndex({ id, ...args }), docPath]; + return [ + await this.createIndex({ + id, + journal: args.journal, + content, + frontMatter, + }), + docPath, + ]; } else { return [id, docPath]; } }; private updateDocument = async (args: SaveRequest): Promise => { - const content = this.contentsWithFrontMatter(args); + if (!args.id) throw new Error("id required to update document"); + + const frontMatter = this.mergedFrontMatter(args); + frontMatter.updatedAt = new Date().toISOString(); + const content = this.prependFrontMatter(args.content, frontMatter); const origDoc = await this.findById({ id: args.id! }); await this.files.uploadDocument({ id: args.id!, content }, args.journal); @@ -211,7 +240,12 @@ updatedAt: ${document.updatedAt} this.updateDependentLinks([args.id!], args.journal); } - return await this.updateIndex(args); + return await this.updateIndex({ + id: args.id!, + content, + journal: args.journal, + frontMatter, + }); }; // todo: also need to update dependent title, if the title of the original note @@ -250,89 +284,71 @@ updatedAt: ${document.updatedAt} createIndex = async ({ id, - createdAt, - updatedAt, journal, content, - title, - tags, - }: SaveRequest): Promise => { + frontMatter, + }: IndexRequest): Promise => { if (!id) { throw new Error("id required to create document index"); } - return this.db.transaction(async () => { - this.db - .prepare( - `INSERT INTO documents (id, journal, content, title, createdAt, updatedAt) VALUES (:id, :journal, :content, :title, :createdAt, :updatedAt)`, - ) - .run({ - id, - journal, - content, - title, - // allow passing createdAt to support backfilling prior notes - createdAt: createdAt || new Date().toISOString(), - updatedAt: updatedAt || new Date().toISOString(), - }); + return this.knex.transaction(async (trx) => { + await trx("documents").insert({ + id, + journal, + content, + title: frontMatter.title, + createdAt: frontMatter.createdAt, + updatedAt: frontMatter.updatedAt, + frontMatter: yaml.stringify(frontMatter || {}), + }); - if (tags.length > 0) { - this.db - .prepare( - `INSERT INTO document_tags (documentId, tag) VALUES ${tags.map((tag) => `(:documentId, '${tag}')`).join(", ")}`, - ) - .run({ documentId: id }); + if (frontMatter.tags.length > 0) { + await trx("document_tags").insert( + frontMatter.tags.map((tag: string) => ({ documentId: id, tag })), + ); } - await this.addNoteLinks(id, content); + await this.addNoteLinks(trx, id, content); return id; - })(); + }); }; updateIndex = async ({ id, - createdAt, - updatedAt, journal, content, - title, - tags, - }: SaveRequest): Promise => { - return this.db.transaction(async () => { - this.db - .prepare( - `UPDATE documents SET journal=:journal, content=:content, title=:title, updatedAt=:updatedAt, createdAt=:createdAt WHERE id=:id`, - ) - .run({ - id, + frontMatter, + }: IndexRequest): Promise => { + return this.knex.transaction(async (trx) => { + await trx("documents") + .update({ content, - title, + title: frontMatter.title, journal, - updatedAt: updatedAt || new Date().toISOString(), - createdAt, - }); - - // re-create tags to avoid diffing - this.db - .prepare(`DELETE FROM document_tags WHERE documentId = :documentId`) - .run({ documentId: id }); - - if (tags.length > 0) { - this.db - .prepare( - `INSERT INTO document_tags (documentId, tag) VALUES ${tags.map((tag) => `(:documentId, '${tag}')`).join(", ")}`, - ) - .run({ documentId: id }); + updatedAt: frontMatter.updatedAt, + frontMatter: yaml.stringify(frontMatter), + }) + .where({ id }); + + await trx("document_tags").where({ documentId: id }).del(); + if (frontMatter.tags.length > 0) { + await trx("document_tags").insert( + frontMatter.tags.map((tag: string) => ({ documentId: id, tag })), + ); } - // re-create note links to avoid diffing - await this.knex("document_links").where({ documentId: id }).del(); - await this.addNoteLinks(id!, content); - })(); + // todo: pass trx to addNoteLinks + await this.addNoteLinks(trx, id!, content); + }); }; - private addNoteLinks = async (documentId: string, content: string) => { + private addNoteLinks = async ( + trx: Knex.Transaction, + documentId: string, + content: string, + ) => { const mdast = parseMarkdown(content); const noteLinks = selectNoteLinks(mdast) .map((link) => parseNoteLink(link.url)) @@ -350,7 +366,7 @@ updatedAt: ${document.updatedAt} }); if (noteLinks.length > 0) { - await this.knex("document_links").insert( + await trx("document_links").insert( noteLinksUnique.map((link) => ({ documentId, targetId: link.noteId, @@ -363,10 +379,8 @@ updatedAt: ${document.updatedAt} /** * When removing a journal, call this to de-index all documents from that journal. */ - deindexJournal = (journal: string): void => { - this.db - .prepare("DELETE FROM documents WHERE journal = :journal") - .run({ journal }); + deindexJournal = (journal: string): Promise => { + return this.knex("documents").where({ journal }).del(); }; /** diff --git a/src/preload/client/importer.ts b/src/preload/client/importer.ts index b5cf66d..f1aebc3 100644 --- a/src/preload/client/importer.ts +++ b/src/preload/client/importer.ts @@ -1,6 +1,7 @@ import { Database } from "better-sqlite3"; import { Knex } from "knex"; import path from "path"; +import yaml from "yaml"; import { Files, PathStatsFile } from "../files"; import { IDocumentsClient } from "./documents"; import { IFilesClient } from "./files"; @@ -222,7 +223,7 @@ export class ImporterClient { importDir, journals, sourceType, - // See notes in inferOrGenerateJournalName; this is a very specific + // See notes in inferOrGenerateJournalName; this is very specific // to my Notion export. frontMatter.Category, ); @@ -256,7 +257,7 @@ export class ImporterClient { title, content: body, journal: journalName, - frontMatter: JSON.stringify(frontMatter), + frontMatter: yaml.stringify(frontMatter), status: "pending", }; @@ -300,7 +301,7 @@ export class ImporterClient { }); for await (const item of items) { - const frontMatter = JSON.parse(item.frontMatter); + const frontMatter = yaml.parse(item.frontMatter); const mdast = stringToMdast(item.content) as any as mdast.Root; await this.updateNoteLinks(mdast, item, linkMapping, wikiLinkMapping); @@ -313,7 +314,7 @@ export class ImporterClient { this.convertWikiLinks(mdast); - // process tags into front matter + // process inline tags into front matter frontMatter.tags = Array.from( new Set(this.processAndConvertTags(mdast, frontMatter.tags || [])), ); @@ -329,6 +330,7 @@ export class ImporterClient { tags: frontMatter.tags || [], createdAt: frontMatter.createdAt, updatedAt: frontMatter.updatedAt, + frontMatter: frontMatter, }, false, // don't index; we'll call sync after import ); diff --git a/src/preload/client/importer/frontmatter.ts b/src/preload/client/importer/frontmatter.ts index b5afe73..64ca612 100644 --- a/src/preload/client/importer/frontmatter.ts +++ b/src/preload/client/importer/frontmatter.ts @@ -1,4 +1,5 @@ import yaml from "yaml"; +import { mdastToString, parseMarkdownForImport } from "../../../markdown"; import { SourceType } from "../importer/SourceType"; interface ParseTitleAndFrontMatterRes { @@ -23,15 +24,64 @@ export const parseTitleAndFrontMatter = ( if (sourceType === "notion") { return parseTitleAndFrontMatterNotion(contents); } else { - // Otherwise for other import types, for now, make no attempt at finding - // or parsing front matter. + return parseTitleAndFrontMatterMarkdown(contents, filename); + } +}; + +function parseTitleAndFrontMatterMarkdown( + contents: string, + filename: string, +): ParseTitleAndFrontMatterRes { + const { frontMatter, body } = extractFronMatter(contents); + return { + title: frontMatter.title || filename, + frontMatter, + body, + }; +} + +function extractFronMatter(contents: string): { + frontMatter: Record; + body: string; +} { + const mdast = parseMarkdownForImport(contents); + if (mdast.children[0].type === "yaml") { + const frontMatter = yaml.parse(mdast.children[0].value); + mdast.children = mdast.children.slice(1); + const contents = mdastToString(mdast); + return { + frontMatter, + body: contents, + }; + } else { return { - title: filename, frontMatter: {}, body: contents, }; } -}; +} + +// extract front matter from content, and return the front matter and body +export function parseChroniclesFrontMatter(content: string) { + const { frontMatter, body } = extractFronMatter(content); + + frontMatter.tags = frontMatter.tags || []; + + // Prior version of Chronicles manually encoded as comma separated tags, + // then re-parsed out. Now using proper yaml parsing, this can be removed + // once all my personal notes are migrated. + if (frontMatter.tags && typeof frontMatter.tags === "string") { + frontMatter.tags = frontMatter.tags + .split(",") + .map((tag: string) => tag.trim()) + .filter(Boolean); + } + + return { + frontMatter, + body, + }; +} /** * Parses a string of contents into a title, front matter, and body; strips title / frontmatter @@ -266,54 +316,3 @@ function preprocessRawFrontMatter(content: string) { }) ); } - -function preprocessChroniclesFrontMatter(content: string) { - // Regular expression to match key-value pairs in front matter - return content - .replace(/^(\w+):\s*$/gm, '$1: ""') // Handle keys with no values - .replace(/^(\w+):\s*(.+)$/gm, (match, key, value) => { - // Check if value contains special characters that need quoting - if (value.match(/[:{}[\],&*#?|\-<>=!%@`]/) || value.includes("\n")) { - // If the value is not already quoted, wrap it in double quotes - if (!/^['"].*['"]$/.test(value)) { - // Escape any existing double quotes in the value - value = value.replace(/"/g, '\\"'); - return `${key}: "${value}"`; - } - } - return match; // Return unchanged if no special characters - }); -} - -// naive frontmatter parser for files formatted in chronicles style... -// which just means a regular markdown file + yaml front matter -// ... todo: use remark ecosystem parser -export function parseChroniclesFrontMatter(content: string) { - // Regular expression to match front matter (--- at the beginning and end) - const frontMatterRegex = /^---\n([\s\S]*?)\n---\n*/; - - // Match the front matter - const match = content.match(frontMatterRegex); - if (!match) { - return { - frontMatter: {}, // No front matter found - body: content, // Original content without changes - }; - } - - // Extract front matter and body - const frontMatterContent = preprocessChroniclesFrontMatter(match[1]); - const body = content.slice(match[0].length); // Content without front matter - - // Parse the front matter using yaml - const frontMatter = yaml.parse(frontMatterContent); - frontMatter.tags = frontMatter.tags - .split(",") - .map((tag: string) => tag.trim()) - .filter(Boolean); - - return { - frontMatter, - body, - }; -} diff --git a/src/preload/client/sync.ts b/src/preload/client/sync.ts index d50dff9..5a21bde 100644 --- a/src/preload/client/sync.ts +++ b/src/preload/client/sync.ts @@ -132,19 +132,12 @@ updatedAt: ${document.updatedAt} const { contents, frontMatter } = await this.documents.loadDoc(file.path); - // todo: handle additional kinds of frontMatter; just add a column for them - // and ensure they are not overwritten when editing existing files - // https://github.com/cloverich/chronicles/issues/127 - try { await this.documents.createIndex({ id: documentId, journal: dirname, // using name as id content: contents, - title: frontMatter.title, - tags: frontMatter.tags || [], - createdAt: frontMatter.createdAt, - updatedAt: frontMatter.updatedAt, + frontMatter, }); syncedCount++; } catch (e) { diff --git a/src/preload/client/types.ts b/src/preload/client/types.ts index bc3f15e..da2c7a0 100644 --- a/src/preload/client/types.ts +++ b/src/preload/client/types.ts @@ -40,6 +40,8 @@ export interface GetDocumentResponse { content: string; journal: string; tags: string[]; + // todo: Define keys required on frontMatter + frontMatter: Record; } /** @@ -126,6 +128,15 @@ export interface SaveRequest { // to support the import process createdAt?: string; updatedAt?: string; + frontMatter: Record; +} + +// todo: Define keys required on frontMatter +export interface IndexRequest { + id: string; + journal: string; + content: string; + frontMatter: Record; } // Nobody would put node_modules in their note directory... right? diff --git a/src/views/create/index.tsx b/src/views/create/index.tsx index 983d711..1b6a4f1 100644 --- a/src/views/create/index.tsx +++ b/src/views/create/index.tsx @@ -70,6 +70,7 @@ function useCreateDocument() { content: "", journal: journal, tags: searchStore.selectedTags, + frontMatter: {}, }); if (!isMounted) return; diff --git a/src/views/edit/EditableDocument.ts b/src/views/edit/EditableDocument.ts index 3bdc5a4..c0f3dd8 100644 --- a/src/views/edit/EditableDocument.ts +++ b/src/views/edit/EditableDocument.ts @@ -49,6 +49,7 @@ export class EditableDocument { @observable createdAt: string; @observable updatedAt: string; // read-only outside this class @observable tags: string[] = []; + @observable frontMatter: Record = {}; // editor properties slateContent: SlateCustom.SlateNode[]; @@ -68,6 +69,7 @@ export class EditableDocument { this.createdAt = doc.createdAt; this.updatedAt = doc.updatedAt; this.tags = doc.tags; + this.frontMatter = doc.frontMatter; const content = doc.content; const slateNodes = SlateTransformer.nodify(content); this.slateContent = slateNodes; @@ -131,6 +133,7 @@ export class EditableDocument { "id", "createdAt", "tags", + "frontMatter", ), ); this.id = res.id; diff --git a/yarn.lock b/yarn.lock index 10618bb..d4077df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2878,6 +2878,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fault@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c" + integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== + dependencies: + format "^0.2.0" + fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -2954,6 +2961,11 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +format@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -3926,6 +3938,18 @@ mdast-util-from-markdown@^2.0.2: micromark-util-types "^2.0.0" unist-util-stringify-position "^4.0.0" +mdast-util-frontmatter@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz#f5f929eb1eb36c8a7737475c7eb438261f964ee8" + integrity sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + escape-string-regexp "^5.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-extension-frontmatter "^2.0.0" + mdast-util-gfm-autolink-literal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz#5baf35407421310a08e68c15e5d8821e8898ba2a" @@ -4067,6 +4091,16 @@ micromark-core-commonmark@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-frontmatter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz#651c52ffa5d7a8eeed687c513cd869885882d67a" + integrity sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg== + dependencies: + fault "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + micromark-extension-gfm-autolink-literal@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz#6286aee9686c4462c1e3552a9d505feddceeb935"