@dropsh/plugin-markdown 0.5.3 → 0.5.6
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.
- package/dist/plugins/markdown/src/index.js +12 -0
- package/dist/plugins/markdown/src/render-md.js +71 -0
- package/package.json +14 -4
- package/src/index.ts +0 -15
- package/src/render-md.ts +0 -80
- package/tests/unit/render-md.test.ts +0 -108
- package/tsconfig.json +0 -8
- package/vitest.config.ts +0 -14
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { renderMarkdown } from "./render-md.js";
|
|
2
|
+
export { renderMarkdown };
|
|
3
|
+
export function markdownPlugin() {
|
|
4
|
+
return {
|
|
5
|
+
id: "markdown",
|
|
6
|
+
requiredModules: [],
|
|
7
|
+
async extendSchema(_entityType, _bundle, schema) {
|
|
8
|
+
return schema;
|
|
9
|
+
},
|
|
10
|
+
renderers: [{ id: "md", render: renderMarkdown }],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { indexIncluded, } from "dropsh/plugin";
|
|
2
|
+
function isScalar(v) {
|
|
3
|
+
return typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
4
|
+
}
|
|
5
|
+
function yamlScalar(v) {
|
|
6
|
+
if (typeof v === "string") {
|
|
7
|
+
return v === "" || v.trim() !== v || /[:#\n"'[\]{}]/.test(v) ? JSON.stringify(v) : v;
|
|
8
|
+
}
|
|
9
|
+
return String(v);
|
|
10
|
+
}
|
|
11
|
+
function isRef(x) {
|
|
12
|
+
return (!!x &&
|
|
13
|
+
typeof x === "object" &&
|
|
14
|
+
typeof x.type === "string" &&
|
|
15
|
+
typeof x.id === "string");
|
|
16
|
+
}
|
|
17
|
+
function relIds(rel) {
|
|
18
|
+
if (!rel || typeof rel !== "object")
|
|
19
|
+
return [];
|
|
20
|
+
const data = rel.data;
|
|
21
|
+
if (Array.isArray(data))
|
|
22
|
+
return data.filter(isRef).map((r) => `${r.type}/${r.id}`);
|
|
23
|
+
if (isRef(data))
|
|
24
|
+
return [`${data.type}/${data.id}`];
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
// Flattens any attribute value to a single-line string for `label: value`
|
|
28
|
+
// output. Text-field objects (`{ processed | value }`) collapse to their text;
|
|
29
|
+
// other objects/arrays fall back to compact JSON.
|
|
30
|
+
function fieldValue(v) {
|
|
31
|
+
if (isScalar(v))
|
|
32
|
+
return yamlScalar(v);
|
|
33
|
+
if (v && typeof v === "object") {
|
|
34
|
+
const o = v;
|
|
35
|
+
if (typeof o.processed === "string")
|
|
36
|
+
return yamlScalar(o.processed);
|
|
37
|
+
if (typeof o.value === "string")
|
|
38
|
+
return yamlScalar(o.value);
|
|
39
|
+
return JSON.stringify(v);
|
|
40
|
+
}
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
// Renders a resource as a flat `label: value` list: type/id first, then every
|
|
44
|
+
// attribute, then relationships as `[type/id]` reference lists.
|
|
45
|
+
function renderResource(res) {
|
|
46
|
+
const lines = [`type: ${res.type}`, `id: ${res.id}`];
|
|
47
|
+
for (const [k, v] of Object.entries(res.attributes ?? {})) {
|
|
48
|
+
lines.push(`${k}: ${fieldValue(v)}`);
|
|
49
|
+
}
|
|
50
|
+
for (const [k, v] of Object.entries(res.relationships ?? {})) {
|
|
51
|
+
const ids = relIds(v);
|
|
52
|
+
if (ids.length)
|
|
53
|
+
lines.push(`${k}: [${ids.join(", ")}]`);
|
|
54
|
+
}
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
export function renderMarkdown(doc, _ctx) {
|
|
58
|
+
const data = doc.data;
|
|
59
|
+
const primary = Array.isArray(data)
|
|
60
|
+
? data.length === 0
|
|
61
|
+
? "_(no results)_"
|
|
62
|
+
: data.map(renderResource).join("\n\n---\n\n")
|
|
63
|
+
: renderResource(data);
|
|
64
|
+
// Resources pulled in via `--include` are appended under an Included section
|
|
65
|
+
// so the related entities are visible, not just their reference ids.
|
|
66
|
+
const included = [...indexIncluded(doc).values()];
|
|
67
|
+
if (included.length === 0)
|
|
68
|
+
return primary;
|
|
69
|
+
const inc = included.map(renderResource).join("\n\n---\n\n");
|
|
70
|
+
return `${primary}\n\n## Included\n\n${inc}`;
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dropsh/plugin-markdown",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/plugins/markdown/src/index.js",
|
|
6
|
+
"types": "./dist/plugins/markdown/src/index.d.ts",
|
|
6
7
|
"exports": {
|
|
7
|
-
".":
|
|
8
|
-
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/plugins/markdown/src/index.d.ts",
|
|
10
|
+
"default": "./dist/plugins/markdown/src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./render": {
|
|
13
|
+
"types": "./dist/plugins/markdown/src/render-md.d.ts",
|
|
14
|
+
"default": "./dist/plugins/markdown/src/render-md.js"
|
|
15
|
+
}
|
|
9
16
|
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/plugins/markdown/src"
|
|
19
|
+
],
|
|
10
20
|
"publishConfig": {
|
|
11
21
|
"access": "public"
|
|
12
22
|
},
|
|
@@ -16,7 +26,7 @@
|
|
|
16
26
|
"devDependencies": {
|
|
17
27
|
"typescript": "^5.6.0",
|
|
18
28
|
"vitest": "^2.1.0",
|
|
19
|
-
"dropsh": "0.5.
|
|
29
|
+
"dropsh": "0.5.6"
|
|
20
30
|
},
|
|
21
31
|
"scripts": {
|
|
22
32
|
"build": "tsc",
|
package/src/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { DropSHPlugin } from "dropsh/plugin";
|
|
2
|
-
import { renderMarkdown } from "./render-md.js";
|
|
3
|
-
|
|
4
|
-
export { renderMarkdown };
|
|
5
|
-
|
|
6
|
-
export function markdownPlugin(): DropSHPlugin {
|
|
7
|
-
return {
|
|
8
|
-
id: "markdown",
|
|
9
|
-
requiredModules: [],
|
|
10
|
-
async extendSchema(_entityType, _bundle, schema) {
|
|
11
|
-
return schema;
|
|
12
|
-
},
|
|
13
|
-
renderers: [{ id: "md", render: renderMarkdown }],
|
|
14
|
-
};
|
|
15
|
-
}
|
package/src/render-md.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
indexIncluded,
|
|
3
|
-
type JsonApiDocument,
|
|
4
|
-
type JsonApiResource,
|
|
5
|
-
type RenderContext,
|
|
6
|
-
} from "dropsh/plugin";
|
|
7
|
-
|
|
8
|
-
type Scalar = string | number | boolean;
|
|
9
|
-
|
|
10
|
-
function isScalar(v: unknown): v is Scalar {
|
|
11
|
-
return typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function yamlScalar(v: Scalar): string {
|
|
15
|
-
if (typeof v === "string") {
|
|
16
|
-
return v === "" || v.trim() !== v || /[:#\n"'[\]{}]/.test(v) ? JSON.stringify(v) : v;
|
|
17
|
-
}
|
|
18
|
-
return String(v);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function isRef(x: unknown): x is { type: string; id: string } {
|
|
22
|
-
return (
|
|
23
|
-
!!x &&
|
|
24
|
-
typeof x === "object" &&
|
|
25
|
-
typeof (x as { type?: unknown }).type === "string" &&
|
|
26
|
-
typeof (x as { id?: unknown }).id === "string"
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function relIds(rel: unknown): string[] {
|
|
31
|
-
if (!rel || typeof rel !== "object") return [];
|
|
32
|
-
const data = (rel as { data?: unknown }).data;
|
|
33
|
-
if (Array.isArray(data)) return data.filter(isRef).map((r) => `${r.type}/${r.id}`);
|
|
34
|
-
if (isRef(data)) return [`${data.type}/${data.id}`];
|
|
35
|
-
return [];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Flattens any attribute value to a single-line string for `label: value`
|
|
39
|
-
// output. Text-field objects (`{ processed | value }`) collapse to their text;
|
|
40
|
-
// other objects/arrays fall back to compact JSON.
|
|
41
|
-
function fieldValue(v: unknown): string {
|
|
42
|
-
if (isScalar(v)) return yamlScalar(v);
|
|
43
|
-
if (v && typeof v === "object") {
|
|
44
|
-
const o = v as Record<string, unknown>;
|
|
45
|
-
if (typeof o.processed === "string") return yamlScalar(o.processed);
|
|
46
|
-
if (typeof o.value === "string") return yamlScalar(o.value);
|
|
47
|
-
return JSON.stringify(v);
|
|
48
|
-
}
|
|
49
|
-
return "";
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Renders a resource as a flat `label: value` list: type/id first, then every
|
|
53
|
-
// attribute, then relationships as `[type/id]` reference lists.
|
|
54
|
-
function renderResource(res: JsonApiResource): string {
|
|
55
|
-
const lines: string[] = [`type: ${res.type}`, `id: ${res.id}`];
|
|
56
|
-
for (const [k, v] of Object.entries(res.attributes ?? {})) {
|
|
57
|
-
lines.push(`${k}: ${fieldValue(v)}`);
|
|
58
|
-
}
|
|
59
|
-
for (const [k, v] of Object.entries(res.relationships ?? {})) {
|
|
60
|
-
const ids = relIds(v);
|
|
61
|
-
if (ids.length) lines.push(`${k}: [${ids.join(", ")}]`);
|
|
62
|
-
}
|
|
63
|
-
return lines.join("\n");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function renderMarkdown(doc: JsonApiDocument, _ctx: RenderContext): string {
|
|
67
|
-
const data = doc.data;
|
|
68
|
-
const primary = Array.isArray(data)
|
|
69
|
-
? data.length === 0
|
|
70
|
-
? "_(no results)_"
|
|
71
|
-
: data.map(renderResource).join("\n\n---\n\n")
|
|
72
|
-
: renderResource(data);
|
|
73
|
-
|
|
74
|
-
// Resources pulled in via `--include` are appended under an Included section
|
|
75
|
-
// so the related entities are visible, not just their reference ids.
|
|
76
|
-
const included = [...indexIncluded(doc).values()];
|
|
77
|
-
if (included.length === 0) return primary;
|
|
78
|
-
const inc = included.map(renderResource).join("\n\n---\n\n");
|
|
79
|
-
return `${primary}\n\n## Included\n\n${inc}`;
|
|
80
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { renderMarkdown } from "../../src/render-md.js";
|
|
3
|
-
|
|
4
|
-
const ctx = { command: "read" as const };
|
|
5
|
-
|
|
6
|
-
describe("renderMarkdown", () => {
|
|
7
|
-
it("appends an Included section for --include'd resources", () => {
|
|
8
|
-
const out = renderMarkdown(
|
|
9
|
-
{
|
|
10
|
-
data: {
|
|
11
|
-
type: "node--article",
|
|
12
|
-
id: "u1",
|
|
13
|
-
attributes: { title: "Primary" },
|
|
14
|
-
relationships: { field_related: { data: { type: "node--article", id: "r1" } } },
|
|
15
|
-
},
|
|
16
|
-
included: [{ type: "node--article", id: "r1", attributes: { title: "Related One" } }],
|
|
17
|
-
},
|
|
18
|
-
ctx,
|
|
19
|
-
);
|
|
20
|
-
expect(out).toContain("title: Primary");
|
|
21
|
-
expect(out).toContain("## Included");
|
|
22
|
-
expect(out).toContain("id: r1");
|
|
23
|
-
expect(out).toContain("title: Related One");
|
|
24
|
-
// primary comes before the included section
|
|
25
|
-
expect(out.indexOf("title: Primary")).toBeLessThan(out.indexOf("## Included"));
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("omits the Included section when there is no included", () => {
|
|
29
|
-
const out = renderMarkdown(
|
|
30
|
-
{ data: { type: "node--article", id: "u1", attributes: { title: "Solo" } } },
|
|
31
|
-
ctx,
|
|
32
|
-
);
|
|
33
|
-
expect(out).not.toContain("## Included");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("renders every field as label: value", () => {
|
|
37
|
-
const out = renderMarkdown(
|
|
38
|
-
{
|
|
39
|
-
data: {
|
|
40
|
-
type: "node--article",
|
|
41
|
-
id: "u1",
|
|
42
|
-
attributes: { title: "Hello", status: true, body: { value: "The text." } },
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
ctx,
|
|
46
|
-
);
|
|
47
|
-
expect(out).toContain("type: node--article");
|
|
48
|
-
expect(out).toContain("id: u1");
|
|
49
|
-
expect(out).toContain("title: Hello");
|
|
50
|
-
expect(out).toContain("status: true");
|
|
51
|
-
// text-field objects collapse to their text, still as label: value
|
|
52
|
-
expect(out).toContain("body: The text.");
|
|
53
|
-
// no frontmatter fence
|
|
54
|
-
expect(out.startsWith("---")).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("does not duplicate any field when there is no body field", () => {
|
|
58
|
-
const out = renderMarkdown(
|
|
59
|
-
{
|
|
60
|
-
data: {
|
|
61
|
-
type: "node--page",
|
|
62
|
-
id: "p1",
|
|
63
|
-
attributes: { title: "T", summary: "short", teaser: "a much longer piece of text here" },
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
ctx,
|
|
67
|
-
);
|
|
68
|
-
expect(out).toContain("title: T");
|
|
69
|
-
expect(out).toContain("summary: short");
|
|
70
|
-
expect(out).toContain("teaser: a much longer piece of text here");
|
|
71
|
-
// each field appears exactly once (no body-fallback duplication)
|
|
72
|
-
expect(out.match(/teaser: /g)).toHaveLength(1);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("lists relationships as type/id arrays", () => {
|
|
76
|
-
const out = renderMarkdown(
|
|
77
|
-
{
|
|
78
|
-
data: {
|
|
79
|
-
type: "node--article",
|
|
80
|
-
id: "u1",
|
|
81
|
-
attributes: {},
|
|
82
|
-
relationships: { uid: { data: { type: "user--user", id: "a1" } } },
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
ctx,
|
|
86
|
-
);
|
|
87
|
-
expect(out).toContain("uid: [user--user/a1]");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("joins a collection with a separator", () => {
|
|
91
|
-
const out = renderMarkdown(
|
|
92
|
-
{
|
|
93
|
-
data: [
|
|
94
|
-
{ type: "node--article", id: "u1", attributes: { title: "A" } },
|
|
95
|
-
{ type: "node--article", id: "u2", attributes: { title: "B" } },
|
|
96
|
-
],
|
|
97
|
-
},
|
|
98
|
-
{ command: "search" },
|
|
99
|
-
);
|
|
100
|
-
expect(out).toContain("id: u1");
|
|
101
|
-
expect(out).toContain("id: u2");
|
|
102
|
-
expect(out).toContain("\n\n---\n\n");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("renders an empty collection as a placeholder", () => {
|
|
106
|
-
expect(renderMarkdown({ data: [] }, { command: "search" })).toBe("_(no results)_");
|
|
107
|
-
});
|
|
108
|
-
});
|
package/tsconfig.json
DELETED
package/vitest.config.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { defineConfig } from "vitest/config";
|
|
3
|
-
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
resolve: {
|
|
6
|
-
alias: {
|
|
7
|
-
"dropsh/plugin": resolve(__dirname, "../../src/plugin-api.ts"),
|
|
8
|
-
},
|
|
9
|
-
},
|
|
10
|
-
test: {
|
|
11
|
-
include: ["tests/unit/**/*.test.ts"],
|
|
12
|
-
environment: "node",
|
|
13
|
-
},
|
|
14
|
-
});
|