@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.
@@ -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, '&amp;')
42
+ .replace(/</g, '&lt;')
43
+ .replace(/>/g, '&gt;');
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
+ }
@@ -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
+ }
@@ -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
+ }