@jxsuite/parser 0.21.4 → 0.22.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/parser",
3
- "version": "0.21.4",
3
+ "version": "0.22.1",
4
4
  "description": "Jx markdown parser and external class integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,9 +14,13 @@
14
14
  "type": "module",
15
15
  "exports": {
16
16
  ".": "./src/md.js",
17
+ "./content": "./src/content.js",
18
+ "./ContentCollection.class.json": "./src/ContentCollection.class.json",
19
+ "./ContentEntry.class.json": "./src/ContentEntry.class.json",
17
20
  "./MarkdownCollection.class.json": "./src/MarkdownCollection.class.json",
18
21
  "./MarkdownFile.class.json": "./src/MarkdownFile.class.json",
19
- "./transpile": "./src/transpile.js"
22
+ "./transpile": "./src/transpile.js",
23
+ "./html-to-jx": "./src/html-to-jx.js"
20
24
  },
21
25
  "publishConfig": {
22
26
  "provenance": true
@@ -44,7 +48,7 @@
44
48
  },
45
49
  "devDependencies": {
46
50
  "@happy-dom/global-registrator": "^20.9.0",
47
- "@jxsuite/runtime": "^0.21.4",
51
+ "@jxsuite/runtime": "^0.22.1",
48
52
  "@types/hast": "^3.0.4"
49
53
  }
50
54
  }
@@ -0,0 +1,112 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "ContentCollection",
4
+ "description": "Query a project content type with filter, sort, and limit",
5
+ "$prototype": "Class",
6
+ "extends": "Object",
7
+ "$implementation": "./content.js",
8
+
9
+ "$defs": {
10
+ "parameters": {
11
+ "contentType": {
12
+ "identifier": "contentType",
13
+ "type": { "type": "string", "enum": { "$ref": "#/$context/contentTypes" } },
14
+ "description": "Content type to query"
15
+ },
16
+ "filter": {
17
+ "identifier": "filter",
18
+ "type": {
19
+ "type": "array",
20
+ "default": [],
21
+ "items": {
22
+ "type": "object",
23
+ "properties": {
24
+ "field": {
25
+ "type": "string",
26
+ "enum": { "$ref": "#/$context/contentTypes/{@contentType}/schema/properties" }
27
+ },
28
+ "op": {
29
+ "type": "string",
30
+ "enum": [
31
+ "==",
32
+ "!=",
33
+ ">",
34
+ "<",
35
+ ">=",
36
+ "<=",
37
+ "contains",
38
+ "not contains",
39
+ "empty",
40
+ "not empty"
41
+ ]
42
+ },
43
+ "value": {}
44
+ },
45
+ "required": ["field", "op"]
46
+ }
47
+ },
48
+ "description": "Filter rules"
49
+ },
50
+ "sort": {
51
+ "identifier": "sort",
52
+ "type": {
53
+ "type": "array",
54
+ "default": [],
55
+ "items": {
56
+ "type": "object",
57
+ "properties": {
58
+ "field": {
59
+ "type": "string",
60
+ "enum": { "$ref": "#/$context/contentTypes/{@contentType}/schema/properties" }
61
+ },
62
+ "order": { "type": "string", "enum": ["asc", "desc"], "default": "asc" }
63
+ },
64
+ "required": ["field"]
65
+ }
66
+ },
67
+ "description": "Sort rules"
68
+ },
69
+ "limit": {
70
+ "identifier": "limit",
71
+ "type": { "type": "integer", "minimum": 1, "default": null },
72
+ "description": "Maximum entries to return"
73
+ }
74
+ },
75
+
76
+ "fields": {
77
+ "config": {
78
+ "role": "field",
79
+ "access": "private",
80
+ "scope": "instance",
81
+ "identifier": "config",
82
+ "type": { "type": "object" },
83
+ "description": "Raw configuration object passed to the constructor"
84
+ }
85
+ },
86
+
87
+ "constructor": {
88
+ "role": "constructor",
89
+ "$prototype": "Function",
90
+ "parameters": [
91
+ { "$ref": "#/$defs/parameters/contentType" },
92
+ { "$ref": "#/$defs/parameters/filter" },
93
+ { "$ref": "#/$defs/parameters/sort" },
94
+ { "$ref": "#/$defs/parameters/limit" }
95
+ ],
96
+ "body": ["this.config = config;"],
97
+ "description": "Stores the configuration for deferred resolution"
98
+ },
99
+
100
+ "methods": {
101
+ "resolve": {
102
+ "role": "method",
103
+ "$prototype": "Function",
104
+ "access": "public",
105
+ "scope": "instance",
106
+ "identifier": "resolve",
107
+ "parameters": [],
108
+ "description": "Query the content type entries with filter, sort, and limit"
109
+ }
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "ContentEntry",
4
+ "description": "Fetch a single entry from a content type by ID",
5
+ "$prototype": "Class",
6
+ "extends": "Object",
7
+ "$implementation": "./content.js",
8
+
9
+ "$defs": {
10
+ "parameters": {
11
+ "contentType": {
12
+ "identifier": "contentType",
13
+ "type": { "type": "string", "enum": { "$ref": "#/$context/contentTypes" } },
14
+ "description": "Content type to query"
15
+ },
16
+ "id": {
17
+ "identifier": "id",
18
+ "type": { "type": "string" },
19
+ "description": "Entry ID to fetch"
20
+ }
21
+ },
22
+
23
+ "fields": {
24
+ "config": {
25
+ "role": "field",
26
+ "access": "private",
27
+ "scope": "instance",
28
+ "identifier": "config",
29
+ "type": { "type": "object" },
30
+ "description": "Raw configuration object passed to the constructor"
31
+ }
32
+ },
33
+
34
+ "constructor": {
35
+ "role": "constructor",
36
+ "$prototype": "Function",
37
+ "parameters": [
38
+ { "$ref": "#/$defs/parameters/contentType" },
39
+ { "$ref": "#/$defs/parameters/id" }
40
+ ],
41
+ "body": ["this.config = config;"],
42
+ "description": "Stores the configuration for deferred resolution"
43
+ },
44
+
45
+ "methods": {
46
+ "resolve": {
47
+ "role": "method",
48
+ "$prototype": "Function",
49
+ "access": "public",
50
+ "scope": "instance",
51
+ "identifier": "resolve",
52
+ "parameters": [],
53
+ "description": "Fetch the entry by ID from the content type"
54
+ }
55
+ }
56
+ }
57
+ }
package/src/content.js ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Content.js — ContentCollection and ContentEntry class implementations
3
+ *
4
+ * Provides the $implementation sidecar for ContentCollection.class.json and
5
+ * ContentEntry.class.json. Also exports the pure query functions (evaluateFilterRule,
6
+ * queryContentType, findEntry) used by both the classes and the server endpoint.
7
+ */
8
+
9
+ // ─── Query Functions ────────────────────────────────────────────────────────
10
+
11
+ /**
12
+ * Evaluate a single filter rule against an entry.
13
+ *
14
+ * @param {{ field: string; op: string; value?: unknown }} rule
15
+ * @param {ContentLoaderEntry} entry
16
+ * @returns {boolean}
17
+ */
18
+ export function evaluateFilterRule(rule, entry) {
19
+ const actual = rule.field === "id" ? entry.id : entry.data[rule.field];
20
+ switch (rule.op) {
21
+ case "==":
22
+ return actual === rule.value;
23
+ case "!=":
24
+ return actual !== rule.value;
25
+ case "empty":
26
+ return actual == null || actual === "" || (Array.isArray(actual) && actual.length === 0);
27
+ case "not empty":
28
+ return actual != null && actual !== "" && !(Array.isArray(actual) && actual.length === 0);
29
+ case "contains":
30
+ return typeof actual === "string" && actual.includes(String(rule.value ?? ""));
31
+ case "not contains":
32
+ return typeof actual === "string" && !actual.includes(String(rule.value ?? ""));
33
+ case ">":
34
+ return /** @type {any} */ (actual) > /** @type {any} */ (rule.value);
35
+ case "<":
36
+ return /** @type {any} */ (actual) < /** @type {any} */ (rule.value);
37
+ case ">=":
38
+ return /** @type {any} */ (actual) >= /** @type {any} */ (rule.value);
39
+ case "<=":
40
+ return /** @type {any} */ (actual) <= /** @type {any} */ (rule.value);
41
+ default:
42
+ return true;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Query a loaded content type with filter, sort, and limit.
48
+ *
49
+ * @param {ContentLoaderEntry[]} entries - Full content type entries
50
+ * @param {{
51
+ * filter?: Record<string, unknown> | Array<{ field: string; op: string; value?: unknown }>;
52
+ * sort?: { field: string; order?: string } | Array<{ field: string; order?: string }>;
53
+ * limit?: number;
54
+ * }} [query]
55
+ * @returns {ContentLoaderEntry[]} Filtered, sorted, limited entries
56
+ */
57
+ export function queryContentType(entries, query = {}) {
58
+ let result = [...entries];
59
+
60
+ // Filter
61
+ if (query.filter) {
62
+ /** @type {{ field: string; op: string; value?: unknown }[]} */
63
+ let rules;
64
+ if (Array.isArray(query.filter)) {
65
+ rules = query.filter;
66
+ } else if (typeof query.filter === "object") {
67
+ rules = Object.entries(query.filter).map(([field, value]) => ({ field, op: "==", value }));
68
+ } else {
69
+ rules = [];
70
+ }
71
+ result = result.filter((entry) => rules.every((rule) => evaluateFilterRule(rule, entry)));
72
+ }
73
+
74
+ // Sort
75
+ if (query.sort) {
76
+ const sortRules = Array.isArray(query.sort) ? query.sort : [query.sort];
77
+ result.sort((a, b) => {
78
+ for (const { field, order = "asc" } of sortRules) {
79
+ const aVal = /** @type {string | number} */ (field === "id" ? a.id : (a.data[field] ?? ""));
80
+ const bVal = /** @type {string | number} */ (field === "id" ? b.id : (b.data[field] ?? ""));
81
+ if (aVal < bVal) return order === "asc" ? -1 : 1;
82
+ if (aVal > bVal) return order === "asc" ? 1 : -1;
83
+ }
84
+ return 0;
85
+ });
86
+ }
87
+
88
+ // Limit
89
+ if (query.limit && query.limit > 0) {
90
+ result = result.slice(0, query.limit);
91
+ }
92
+
93
+ return result;
94
+ }
95
+
96
+ /**
97
+ * Find a single entry by ID in a content type.
98
+ *
99
+ * @param {ContentLoaderEntry[]} entries - Full content type entries
100
+ * @param {string} id - Entry ID to find
101
+ * @returns {ContentLoaderEntry | null} The matching entry or null
102
+ */
103
+ export function findEntry(entries, id) {
104
+ return entries.find((e) => e.id === id) ?? null;
105
+ }
106
+
107
+ // ─── Class Implementations ──────────────────────────────────────────────────
108
+
109
+ /**
110
+ * @typedef {{
111
+ * contentType?: string;
112
+ * filter?: unknown;
113
+ * sort?: unknown;
114
+ * limit?: number;
115
+ * _project?: { contentTypes?: Map<string, ContentLoaderEntry[]>; [k: string]: unknown };
116
+ * [k: string]: unknown;
117
+ * }} CollectionConfig
118
+ *
119
+ * @typedef {{
120
+ * contentType?: string;
121
+ * id?: unknown;
122
+ * _project?: { contentTypes?: Map<string, ContentLoaderEntry[]>; [k: string]: unknown };
123
+ * _document?: { route?: { _pathParams?: Record<string, string> }; [k: string]: unknown };
124
+ * [k: string]: unknown;
125
+ * }} EntryConfig
126
+ */
127
+
128
+ export class ContentCollection {
129
+ /** @param {CollectionConfig} config */
130
+ constructor(config) {
131
+ this.config = config;
132
+ }
133
+
134
+ resolve() {
135
+ const { contentType, filter, sort, limit, _project } = this.config;
136
+ const entries = _project?.contentTypes?.get(contentType ?? "");
137
+ if (!entries) return [];
138
+ return queryContentType(entries, /** @type {any} */ ({ filter, sort, limit }));
139
+ }
140
+ }
141
+
142
+ export class ContentEntry {
143
+ /** @param {EntryConfig} config */
144
+ constructor(config) {
145
+ this.config = config;
146
+ }
147
+
148
+ resolve() {
149
+ const { contentType, id, _project, _document } = this.config;
150
+ const entries = _project?.contentTypes?.get(contentType ?? "");
151
+ if (!entries) return null;
152
+
153
+ let resolvedId = id;
154
+ if (
155
+ resolvedId &&
156
+ typeof resolvedId === "object" &&
157
+ /** @type {{ $ref?: string }} */ (resolvedId).$ref?.startsWith("#/$params/")
158
+ ) {
159
+ const paramName = /** @type {{ $ref: string }} */ (resolvedId).$ref.replace("#/$params/", "");
160
+ resolvedId = _document?.route?._pathParams?.[paramName];
161
+ }
162
+ if (!resolvedId) return null;
163
+
164
+ return findEntry(entries, /** @type {string} */ (resolvedId));
165
+ }
166
+ }