@zenithbuild/plugins 0.3.2
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/.github/workflows/release.yml +254 -0
- package/.releaserc.json +73 -0
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/bun.lock +17 -0
- package/content/README.md +116 -0
- package/content/enhancers.ts +39 -0
- package/content/hooks/useZenOrder.ts +292 -0
- package/content/index.ts +209 -0
- package/content/loader.ts +165 -0
- package/content/markdown.ts +244 -0
- package/content/query.ts +100 -0
- package/content/schema.ts +30 -0
- package/content/types.ts +53 -0
- package/package.json +36 -0
- package/scripts/release.ts +554 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown to Zenith Render Nodes Compiler
|
|
3
|
+
*
|
|
4
|
+
* Converts markdown content to Zenith-compatible VNodes at build/load time.
|
|
5
|
+
* This ensures markdown is fully compiled before reaching the frontend.
|
|
6
|
+
*
|
|
7
|
+
* Supported syntax:
|
|
8
|
+
* - Headings (#, ##, ###, ####, #####, ######)
|
|
9
|
+
* - Paragraphs
|
|
10
|
+
* - Bold (**text** or __text__)
|
|
11
|
+
* - Italic (*text* or _text_)
|
|
12
|
+
* - Inline code (`code`)
|
|
13
|
+
* - Code blocks (```language\ncode\n```)
|
|
14
|
+
* - Links ([text](url))
|
|
15
|
+
* - Horizontal rules (---, ***, ___)
|
|
16
|
+
* - Lists (- item, * item, 1. item)
|
|
17
|
+
* - Blockquotes (> text)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* VNode - Virtual DOM node structure compatible with Zenith runtime
|
|
22
|
+
*/
|
|
23
|
+
export interface VNode {
|
|
24
|
+
tag: string;
|
|
25
|
+
props: Record<string, string | null>;
|
|
26
|
+
children: (VNode | string)[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a VNode element
|
|
31
|
+
*/
|
|
32
|
+
function h(tag: string, props: Record<string, string | null> = {}, children: (VNode | string)[] = []): VNode {
|
|
33
|
+
return { tag, props, children };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Escape HTML entities in text
|
|
38
|
+
*/
|
|
39
|
+
function escapeText(text: string): string {
|
|
40
|
+
return text
|
|
41
|
+
.replace(/&/g, '&')
|
|
42
|
+
.replace(/</g, '<')
|
|
43
|
+
.replace(/>/g, '>');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse inline markdown (bold, italic, code, links)
|
|
48
|
+
*/
|
|
49
|
+
function parseInline(text: string): (VNode | string)[] {
|
|
50
|
+
const result: (VNode | string)[] = [];
|
|
51
|
+
let remaining = text;
|
|
52
|
+
|
|
53
|
+
while (remaining.length > 0) {
|
|
54
|
+
// Bold: **text** or __text__
|
|
55
|
+
let match = remaining.match(/^(\*\*|__)(.+?)\1/);
|
|
56
|
+
if (match) {
|
|
57
|
+
result.push(h('strong', {}, [match[2]]));
|
|
58
|
+
remaining = remaining.slice(match[0].length);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Italic: *text* or _text_ (but not ** or __)
|
|
63
|
+
match = remaining.match(/^(\*|_)(?!\1)(.+?)\1(?!\1)/);
|
|
64
|
+
if (match) {
|
|
65
|
+
result.push(h('em', {}, [match[2]]));
|
|
66
|
+
remaining = remaining.slice(match[0].length);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Inline code: `code`
|
|
71
|
+
match = remaining.match(/^`([^`]+)`/);
|
|
72
|
+
if (match) {
|
|
73
|
+
result.push(h('code', {}, [match[1]]));
|
|
74
|
+
remaining = remaining.slice(match[0].length);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Links: [text](url)
|
|
79
|
+
match = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
|
80
|
+
if (match) {
|
|
81
|
+
result.push(h('a', { href: match[2] }, [match[1]]));
|
|
82
|
+
remaining = remaining.slice(match[0].length);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Find next special character or consume until end
|
|
87
|
+
const nextSpecial = remaining.slice(1).search(/[\*_`\[]/);
|
|
88
|
+
if (nextSpecial === -1) {
|
|
89
|
+
result.push(remaining);
|
|
90
|
+
break;
|
|
91
|
+
} else {
|
|
92
|
+
result.push(remaining.slice(0, nextSpecial + 1));
|
|
93
|
+
remaining = remaining.slice(nextSpecial + 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Compile markdown string to VNode array
|
|
102
|
+
*/
|
|
103
|
+
export function compileMarkdown(markdown: string): VNode[] {
|
|
104
|
+
const nodes: VNode[] = [];
|
|
105
|
+
const lines = markdown.split('\n');
|
|
106
|
+
let i = 0;
|
|
107
|
+
|
|
108
|
+
while (i < lines.length) {
|
|
109
|
+
const line = lines[i];
|
|
110
|
+
const trimmed = line.trim();
|
|
111
|
+
|
|
112
|
+
// Skip empty lines
|
|
113
|
+
if (trimmed === '') {
|
|
114
|
+
i++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Code blocks: ```language\ncode\n```
|
|
119
|
+
if (trimmed.startsWith('```')) {
|
|
120
|
+
const lang = trimmed.slice(3).trim() || null;
|
|
121
|
+
const codeLines: string[] = [];
|
|
122
|
+
i++;
|
|
123
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
124
|
+
codeLines.push(lines[i]);
|
|
125
|
+
i++;
|
|
126
|
+
}
|
|
127
|
+
i++; // Skip closing ```
|
|
128
|
+
|
|
129
|
+
const codeContent = codeLines.join('\n');
|
|
130
|
+
const codeNode = h('code', lang ? { class: `language-${lang}` } : {}, [codeContent]);
|
|
131
|
+
nodes.push(h('pre', {}, [codeNode]));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Headings: # to ######
|
|
136
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
137
|
+
if (headingMatch) {
|
|
138
|
+
const level = headingMatch[1].length;
|
|
139
|
+
const text = headingMatch[2];
|
|
140
|
+
nodes.push(h(`h${level}`, {}, parseInline(text)));
|
|
141
|
+
i++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Horizontal rules: ---, ***, ___
|
|
146
|
+
if (/^([-*_])\1{2,}$/.test(trimmed)) {
|
|
147
|
+
nodes.push(h('hr', {}, []));
|
|
148
|
+
i++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Blockquotes: > text
|
|
153
|
+
if (trimmed.startsWith('>')) {
|
|
154
|
+
const quoteLines: string[] = [];
|
|
155
|
+
while (i < lines.length && lines[i].trim().startsWith('>')) {
|
|
156
|
+
quoteLines.push(lines[i].trim().slice(1).trim());
|
|
157
|
+
i++;
|
|
158
|
+
}
|
|
159
|
+
const quoteContent = quoteLines.join(' ');
|
|
160
|
+
nodes.push(h('blockquote', {}, parseInline(quoteContent)));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Unordered lists: - item, * item
|
|
165
|
+
if (/^[-*]\s+/.test(trimmed)) {
|
|
166
|
+
const listItems: VNode[] = [];
|
|
167
|
+
while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) {
|
|
168
|
+
const itemText = lines[i].trim().replace(/^[-*]\s+/, '');
|
|
169
|
+
listItems.push(h('li', {}, parseInline(itemText)));
|
|
170
|
+
i++;
|
|
171
|
+
}
|
|
172
|
+
nodes.push(h('ul', {}, listItems));
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Ordered lists: 1. item
|
|
177
|
+
if (/^\d+\.\s+/.test(trimmed)) {
|
|
178
|
+
const listItems: VNode[] = [];
|
|
179
|
+
while (i < lines.length && /^\d+\.\s+/.test(lines[i].trim())) {
|
|
180
|
+
const itemText = lines[i].trim().replace(/^\d+\.\s+/, '');
|
|
181
|
+
listItems.push(h('li', {}, parseInline(itemText)));
|
|
182
|
+
i++;
|
|
183
|
+
}
|
|
184
|
+
nodes.push(h('ol', {}, listItems));
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Paragraph: collect consecutive non-empty lines
|
|
189
|
+
const paraLines: string[] = [];
|
|
190
|
+
while (i < lines.length && lines[i].trim() !== '' &&
|
|
191
|
+
!lines[i].trim().startsWith('#') &&
|
|
192
|
+
!lines[i].trim().startsWith('```') &&
|
|
193
|
+
!lines[i].trim().startsWith('>') &&
|
|
194
|
+
!/^[-*]\s+/.test(lines[i].trim()) &&
|
|
195
|
+
!/^\d+\.\s+/.test(lines[i].trim()) &&
|
|
196
|
+
!/^([-*_])\1{2,}$/.test(lines[i].trim())) {
|
|
197
|
+
paraLines.push(lines[i].trim());
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (paraLines.length > 0) {
|
|
202
|
+
const paraText = paraLines.join(' ');
|
|
203
|
+
nodes.push(h('p', {}, parseInline(paraText)));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return nodes;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Convert VNode tree to HTML string for rendering in innerHTML
|
|
212
|
+
* This is used as an intermediate step for browser rendering
|
|
213
|
+
*/
|
|
214
|
+
export function vnodesToHtml(nodes: VNode[]): string {
|
|
215
|
+
function renderNode(node: VNode | string): string {
|
|
216
|
+
if (typeof node === 'string') {
|
|
217
|
+
return escapeText(node);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const { tag, props, children } = node;
|
|
221
|
+
|
|
222
|
+
// Self-closing tags
|
|
223
|
+
if (tag === 'hr' || tag === 'br') {
|
|
224
|
+
const attrs = Object.entries(props)
|
|
225
|
+
.filter(([_, v]) => v !== null)
|
|
226
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
227
|
+
.join(' ');
|
|
228
|
+
return attrs ? `<${tag} ${attrs} />` : `<${tag} />`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const attrs = Object.entries(props)
|
|
232
|
+
.filter(([_, v]) => v !== null)
|
|
233
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
234
|
+
.join(' ');
|
|
235
|
+
|
|
236
|
+
const childrenHtml = children.map(renderNode).join('');
|
|
237
|
+
|
|
238
|
+
return attrs
|
|
239
|
+
? `<${tag} ${attrs}>${childrenHtml}</${tag}>`
|
|
240
|
+
: `<${tag}>${childrenHtml}</${tag}>`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return nodes.map(renderNode).join('\n');
|
|
244
|
+
}
|
package/content/query.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ContentItem, SortOrder, Enhancer } from './types';
|
|
2
|
+
import { applyEnhancers } from './enhancers';
|
|
3
|
+
|
|
4
|
+
export class ZenCollection {
|
|
5
|
+
private collectionItems: ContentItem[];
|
|
6
|
+
private filters: ((item: ContentItem) => boolean)[] = [];
|
|
7
|
+
private sortField: string | null = null;
|
|
8
|
+
private sortOrder: SortOrder = 'desc';
|
|
9
|
+
private limitCount: number | null = null;
|
|
10
|
+
private selectedFields: string[] | null = null;
|
|
11
|
+
private enhancers: Enhancer[] = [];
|
|
12
|
+
|
|
13
|
+
constructor(items: ContentItem[]) {
|
|
14
|
+
this.collectionItems = [...items];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
where(fn: (item: ContentItem) => boolean): this {
|
|
18
|
+
this.filters.push(fn);
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
sortBy(field: string, order: SortOrder = 'desc'): this {
|
|
23
|
+
this.sortField = field;
|
|
24
|
+
this.sortOrder = order;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
limit(n: number): this {
|
|
29
|
+
this.limitCount = n;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fields(fields: string[]): this {
|
|
34
|
+
this.selectedFields = fields;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
enhanceWith(enhancer: Enhancer): this {
|
|
39
|
+
this.enhancers.push(enhancer);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async get(): Promise<ContentItem[]> {
|
|
44
|
+
let results = [...this.collectionItems];
|
|
45
|
+
|
|
46
|
+
// 1. Filter
|
|
47
|
+
for (const filter of this.filters) {
|
|
48
|
+
results = results.filter(filter);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Sort
|
|
52
|
+
if (this.sortField) {
|
|
53
|
+
results.sort((a, b) => {
|
|
54
|
+
const valA = a[this.sortField!];
|
|
55
|
+
const valB = b[this.sortField!];
|
|
56
|
+
|
|
57
|
+
if (valA < valB) return this.sortOrder === 'asc' ? -1 : 1;
|
|
58
|
+
if (valA > valB) return this.sortOrder === 'asc' ? 1 : -1;
|
|
59
|
+
return 0;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 3. Limit
|
|
64
|
+
if (this.limitCount !== null) {
|
|
65
|
+
results = results.slice(0, this.limitCount);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 4. Enhance
|
|
69
|
+
if (this.enhancers.length > 0) {
|
|
70
|
+
results = await Promise.all(results.map(item => applyEnhancers(item, this.enhancers)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 5. Select Fields
|
|
74
|
+
if (this.selectedFields) {
|
|
75
|
+
results = results.map(item => {
|
|
76
|
+
const newItem: any = {};
|
|
77
|
+
this.selectedFields!.forEach(f => {
|
|
78
|
+
newItem[f] = item[f];
|
|
79
|
+
});
|
|
80
|
+
return newItem as ContentItem;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async all(): Promise<ContentItem[]> {
|
|
88
|
+
return this.get();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async first(): Promise<ContentItem | null> {
|
|
92
|
+
const results = await this.limit(1).get();
|
|
93
|
+
return results.length > 0 ? results[0] : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async count(): Promise<number> {
|
|
97
|
+
const results = await this.get();
|
|
98
|
+
return results.length;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ContentSchema } from './types';
|
|
2
|
+
|
|
3
|
+
const schemaRegistry = new Map<string, ContentSchema>();
|
|
4
|
+
|
|
5
|
+
export function defineSchema(name: string, schema: ContentSchema) {
|
|
6
|
+
schemaRegistry.set(name, schema);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getSchema(name: string): ContentSchema | undefined {
|
|
10
|
+
return schemaRegistry.get(name);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validateItem(collection: string, item: any): boolean {
|
|
14
|
+
const schema = getSchema(collection);
|
|
15
|
+
if (!schema) return true; // No schema, no validation (or permissive)
|
|
16
|
+
|
|
17
|
+
// Basic validation logic
|
|
18
|
+
for (const [key, type] of Object.entries(schema)) {
|
|
19
|
+
const value = item[key];
|
|
20
|
+
if (value === undefined) continue; // Optional by default?
|
|
21
|
+
|
|
22
|
+
if (type === 'string' && typeof value !== 'string') return false;
|
|
23
|
+
if (type === 'number' && typeof value !== 'number') return false;
|
|
24
|
+
if (type === 'boolean' && typeof value !== 'boolean') return false;
|
|
25
|
+
if (type === 'string[]' && (!Array.isArray(value) || !value.every(v => typeof v === 'string'))) return false;
|
|
26
|
+
// ... more types
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return true;
|
|
30
|
+
}
|
package/content/types.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface ContentItem {
|
|
2
|
+
id?: string | number;
|
|
3
|
+
slug?: string | null;
|
|
4
|
+
collection?: string | null;
|
|
5
|
+
content?: string | null;
|
|
6
|
+
[key: string]: any | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for a content source
|
|
11
|
+
*/
|
|
12
|
+
export interface ContentSourceConfig {
|
|
13
|
+
/** Root directory relative to project root (e.g., "../zenith-docs" or "content") */
|
|
14
|
+
root: string;
|
|
15
|
+
/** Folders to include from the root (e.g., ["documentation"]). Defaults to all. */
|
|
16
|
+
include?: string[];
|
|
17
|
+
/** Folders to exclude from the root (e.g., ["changelog"]) */
|
|
18
|
+
exclude?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for the content plugin factory function
|
|
23
|
+
*/
|
|
24
|
+
export interface ContentPluginOptions {
|
|
25
|
+
/** Named content sources mapped to their configuration */
|
|
26
|
+
sources?: Record<string, ContentSourceConfig>;
|
|
27
|
+
/** Legacy: Single content directory (deprecated, use sources instead) */
|
|
28
|
+
contentDir?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ContentSchema = Record<string, 'string' | 'number' | 'boolean' | 'string[]' | 'date'>;
|
|
32
|
+
|
|
33
|
+
export interface PluginOptions {
|
|
34
|
+
contentDir?: string; // Default: 'content'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SortOrder = 'asc' | 'desc';
|
|
38
|
+
|
|
39
|
+
export type EnhancerFn = (item: ContentItem) => ContentItem | Promise<ContentItem>;
|
|
40
|
+
export type Enhancer = string | EnhancerFn;
|
|
41
|
+
|
|
42
|
+
export interface PluginContext {
|
|
43
|
+
options: PluginOptions;
|
|
44
|
+
// Add other context methods if known, for now minimal
|
|
45
|
+
[key: string]: any;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ZenithPlugin {
|
|
49
|
+
name: string;
|
|
50
|
+
template?: string;
|
|
51
|
+
options?: PluginOptions;
|
|
52
|
+
setup: (ctx: PluginContext) => void | Promise<void>;
|
|
53
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zenithbuild/plugins",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "Plugin system for Zenith framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "content/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "bun dev",
|
|
9
|
+
"build": "bun build",
|
|
10
|
+
"start": "bun run build && bun run dev",
|
|
11
|
+
"release": "bun run scripts/release.ts",
|
|
12
|
+
"release:dry": "bun run scripts/release.ts --dry-run",
|
|
13
|
+
"release:patch": "bun run scripts/release.ts --bump=patch",
|
|
14
|
+
"release:minor": "bun run scripts/release.ts --bump=minor",
|
|
15
|
+
"release:major": "bun run scripts/release.ts --bump=major"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git@github.com:zenithbuild/zenith-plugins.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"zenith",
|
|
23
|
+
"plugins",
|
|
24
|
+
"framework"
|
|
25
|
+
],
|
|
26
|
+
"author": "Zenith Team",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"private": false,
|
|
32
|
+
"dependencies": {},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^25.0.6"
|
|
35
|
+
}
|
|
36
|
+
}
|