@md-meta-view/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ export { collectFrontmatterKeys, parseDirectory, parseMdFile, } from "./parser.js";
2
+ export { loadSettings } from "./settings.js";
3
+ export type { MdData, MdEntry, Settings } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { collectFrontmatterKeys, parseDirectory, parseMdFile, } from "./parser.js";
2
+ export { loadSettings } from "./settings.js";
@@ -0,0 +1,4 @@
1
+ import type { MdEntry, Settings } from "./types.js";
2
+ export declare function parseMdFile(filePath: string, baseDir: string, settings: Settings): Promise<MdEntry>;
3
+ export declare function parseDirectory(dir: string, settings?: Settings): Promise<MdEntry[]>;
4
+ export declare function collectFrontmatterKeys(entries: MdEntry[]): string[];
package/dist/parser.js ADDED
@@ -0,0 +1,75 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import matter from "gray-matter";
4
+ import picomatch from "picomatch";
5
+ import rehypeHighlight from "rehype-highlight";
6
+ import rehypeSanitize from "rehype-sanitize";
7
+ import rehypeStringify from "rehype-stringify";
8
+ import remarkGfm from "remark-gfm";
9
+ import remarkParse from "remark-parse";
10
+ import remarkRehype from "remark-rehype";
11
+ import { unified } from "unified";
12
+ const MAX_DEPTH = 5;
13
+ const markdownProcessor = unified()
14
+ .use(remarkParse)
15
+ .use(remarkGfm)
16
+ .use(remarkRehype)
17
+ .use(rehypeSanitize)
18
+ .use(rehypeHighlight)
19
+ .use(rehypeStringify);
20
+ function collectMdFiles(dir, baseDir, depth) {
21
+ if (depth > MAX_DEPTH)
22
+ return [];
23
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
24
+ const files = [];
25
+ for (const entry of entries) {
26
+ const fullPath = path.join(dir, entry.name);
27
+ if (entry.isDirectory() &&
28
+ !entry.name.startsWith(".") &&
29
+ entry.name !== "node_modules") {
30
+ files.push(...collectMdFiles(fullPath, baseDir, depth + 1));
31
+ }
32
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
33
+ files.push(fullPath);
34
+ }
35
+ }
36
+ return files;
37
+ }
38
+ export async function parseMdFile(filePath, baseDir, settings) {
39
+ const raw = fs.readFileSync(filePath, "utf-8");
40
+ const { data, content } = matter(raw);
41
+ const result = await markdownProcessor.process(content);
42
+ const relativePath = path.relative(baseDir, filePath);
43
+ const id = settings.idField && data[settings.idField] != null
44
+ ? String(data[settings.idField])
45
+ : relativePath;
46
+ return {
47
+ id,
48
+ filename: path.basename(filePath),
49
+ relativePath,
50
+ frontmatter: data,
51
+ html: String(result),
52
+ };
53
+ }
54
+ export async function parseDirectory(dir, settings = {}) {
55
+ const absDir = path.resolve(dir);
56
+ let files = collectMdFiles(absDir, absDir, 0);
57
+ if (settings.exclude && settings.exclude.length > 0) {
58
+ const isExcluded = picomatch(settings.exclude);
59
+ files = files.filter((f) => {
60
+ const rel = path.relative(absDir, f);
61
+ return !isExcluded(rel);
62
+ });
63
+ }
64
+ const entries = await Promise.all(files.map((f) => parseMdFile(f, absDir, settings)));
65
+ return entries;
66
+ }
67
+ export function collectFrontmatterKeys(entries) {
68
+ const keys = new Set();
69
+ for (const entry of entries) {
70
+ for (const key of Object.keys(entry.frontmatter)) {
71
+ keys.add(key);
72
+ }
73
+ }
74
+ return Array.from(keys);
75
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,92 @@
1
+ import path from "node:path";
2
+ import { describe, expect, it } from "vitest";
3
+ import { collectFrontmatterKeys, parseDirectory, parseMdFile } from "./parser.js";
4
+ const fixturesDir = path.resolve(import.meta.dirname, "../fixtures");
5
+ describe("parseMdFile", () => {
6
+ it("extracts frontmatter and renders HTML", async () => {
7
+ const filePath = path.join(fixturesDir, "basic.md");
8
+ const entry = await parseMdFile(filePath, fixturesDir, {});
9
+ expect(entry.filename).toBe("basic.md");
10
+ expect(entry.relativePath).toBe("basic.md");
11
+ expect(entry.frontmatter.title).toBe("Test Document");
12
+ expect(entry.frontmatter.date).toEqual(new Date("2026-01-15"));
13
+ expect(entry.frontmatter.category).toBe("testing");
14
+ expect(entry.frontmatter.tags).toEqual(["unit", "integration"]);
15
+ expect(entry.html).toContain("<h1>");
16
+ expect(entry.html).toContain("Test Document");
17
+ });
18
+ it("uses idField from settings when available", async () => {
19
+ const filePath = path.join(fixturesDir, "basic.md");
20
+ const entry = await parseMdFile(filePath, fixturesDir, { idField: "number" });
21
+ expect(entry.id).toBe("001");
22
+ });
23
+ it("falls back to relativePath when idField is not set", async () => {
24
+ const filePath = path.join(fixturesDir, "basic.md");
25
+ const entry = await parseMdFile(filePath, fixturesDir, {});
26
+ expect(entry.id).toBe("basic.md");
27
+ });
28
+ it("falls back to relativePath when idField is missing from frontmatter", async () => {
29
+ const filePath = path.join(fixturesDir, "no-frontmatter.md");
30
+ const entry = await parseMdFile(filePath, fixturesDir, { idField: "number" });
31
+ expect(entry.id).toBe("no-frontmatter.md");
32
+ });
33
+ it("handles files without frontmatter", async () => {
34
+ const filePath = path.join(fixturesDir, "no-frontmatter.md");
35
+ const entry = await parseMdFile(filePath, fixturesDir, {});
36
+ expect(entry.frontmatter).toEqual({});
37
+ expect(entry.html).toContain("No Frontmatter");
38
+ });
39
+ it("resolves relativePath for nested files", async () => {
40
+ const filePath = path.join(fixturesDir, "sub/nested.md");
41
+ const entry = await parseMdFile(filePath, fixturesDir, {});
42
+ expect(entry.relativePath).toBe(path.join("sub", "nested.md"));
43
+ });
44
+ });
45
+ describe("parseDirectory", () => {
46
+ it("finds all markdown files recursively", async () => {
47
+ const entries = await parseDirectory(fixturesDir);
48
+ const filenames = entries.map((e) => e.filename).sort();
49
+ expect(filenames).toContain("basic.md");
50
+ expect(filenames).toContain("nested.md");
51
+ expect(filenames).toContain("no-frontmatter.md");
52
+ expect(filenames).toContain("excluded.md");
53
+ });
54
+ it("applies exclude patterns from settings", async () => {
55
+ const entries = await parseDirectory(fixturesDir, { exclude: ["excluded.md"] });
56
+ const filenames = entries.map((e) => e.filename);
57
+ expect(filenames).not.toContain("excluded.md");
58
+ expect(filenames).toContain("basic.md");
59
+ });
60
+ it("applies glob exclude patterns for subdirectories", async () => {
61
+ const entries = await parseDirectory(fixturesDir, { exclude: ["sub/**"] });
62
+ const filenames = entries.map((e) => e.filename);
63
+ expect(filenames).not.toContain("nested.md");
64
+ expect(filenames).toContain("basic.md");
65
+ });
66
+ it("uses idField for all entries", async () => {
67
+ const entries = await parseDirectory(fixturesDir, { idField: "number" });
68
+ const basic = entries.find((e) => e.filename === "basic.md");
69
+ const nested = entries.find((e) => e.filename === "nested.md");
70
+ const noFm = entries.find((e) => e.filename === "no-frontmatter.md");
71
+ expect(basic?.id).toBe("001");
72
+ expect(nested?.id).toBe("002");
73
+ expect(noFm?.id).toBe("no-frontmatter.md");
74
+ });
75
+ });
76
+ describe("collectFrontmatterKeys", () => {
77
+ it("collects unique keys from all entries", async () => {
78
+ const entries = await parseDirectory(fixturesDir);
79
+ const keys = collectFrontmatterKeys(entries);
80
+ expect(keys).toContain("title");
81
+ expect(keys).toContain("date");
82
+ expect(keys).toContain("category");
83
+ expect(keys).toContain("tags");
84
+ expect(keys).toContain("number");
85
+ });
86
+ it("returns empty array for entries without frontmatter", () => {
87
+ const keys = collectFrontmatterKeys([
88
+ { id: "1", filename: "a.md", relativePath: "a.md", frontmatter: {}, html: "" },
89
+ ]);
90
+ expect(keys).toEqual([]);
91
+ });
92
+ });
@@ -0,0 +1,2 @@
1
+ import type { Settings } from "./types.js";
2
+ export declare function loadSettings(dir: string): Settings;
@@ -0,0 +1,16 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const SETTINGS_FILE = "md-meta-view-setting.json";
4
+ export function loadSettings(dir) {
5
+ const filePath = path.resolve(dir, SETTINGS_FILE);
6
+ if (!fs.existsSync(filePath))
7
+ return {};
8
+ try {
9
+ const raw = fs.readFileSync(filePath, "utf-8");
10
+ return JSON.parse(raw);
11
+ }
12
+ catch (e) {
13
+ console.warn(`Warning: Failed to parse ${SETTINGS_FILE}: ${e.message}`);
14
+ return {};
15
+ }
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { loadSettings } from "./settings.js";
6
+ const fixturesDir = path.resolve(import.meta.dirname, "../fixtures");
7
+ describe("loadSettings", () => {
8
+ it("loads settings from md-meta-view-setting.json", () => {
9
+ const settings = loadSettings(fixturesDir);
10
+ expect(settings.idField).toBe("number");
11
+ expect(settings.exclude).toEqual(["excluded.md"]);
12
+ });
13
+ it("returns empty object when file does not exist", () => {
14
+ const settings = loadSettings("/nonexistent/path");
15
+ expect(settings).toEqual({});
16
+ });
17
+ describe("with invalid JSON", () => {
18
+ let tmpDir;
19
+ beforeEach(() => {
20
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "md-meta-view-test-"));
21
+ fs.writeFileSync(path.join(tmpDir, "md-meta-view-setting.json"), "{ invalid json");
22
+ });
23
+ afterEach(() => {
24
+ fs.rmSync(tmpDir, { recursive: true });
25
+ });
26
+ it("returns empty object and warns on invalid JSON", () => {
27
+ const settings = loadSettings(tmpDir);
28
+ expect(settings).toEqual({});
29
+ });
30
+ });
31
+ });
@@ -0,0 +1,16 @@
1
+ export interface MdEntry {
2
+ id: string;
3
+ filename: string;
4
+ relativePath: string;
5
+ frontmatter: Record<string, unknown>;
6
+ html: string;
7
+ }
8
+ export interface Settings {
9
+ idField?: string;
10
+ exclude?: string[];
11
+ }
12
+ export interface MdData {
13
+ entries: MdEntry[];
14
+ keys: string[];
15
+ settings: Settings;
16
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@md-meta-view/core",
3
+ "version": "0.1.0",
4
+ "description": "Core types and markdown parser for md-meta-view",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/ShuntaToda/md-meta-view.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.js",
20
+ "types": "./dist/index.d.ts"
21
+ }
22
+ },
23
+ "scripts": {
24
+ "build": "tsc"
25
+ },
26
+ "dependencies": {
27
+ "gray-matter": "^4.0.3",
28
+ "picomatch": "^4.0.4",
29
+ "unified": "^11.0.5",
30
+ "remark-parse": "^11.0.0",
31
+ "remark-gfm": "^4.0.1",
32
+ "remark-rehype": "^11.1.2",
33
+ "rehype-sanitize": "^6.0.0",
34
+ "rehype-highlight": "^7.0.2",
35
+ "rehype-stringify": "^10.0.1"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^24.12.0",
39
+ "@types/picomatch": "^4.0.2",
40
+ "typescript": "~5.9.3"
41
+ }
42
+ }