@uurtech/jdf-cli 0.1.4 → 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +202 -18
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1,31 +1,63 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'fs';
3
- import path from 'path';
3
+ import path2 from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import Ajv from 'ajv';
6
6
  import addFormats from 'ajv-formats';
7
+ import JSZip from 'jszip';
7
8
 
8
- var __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
9
+ // ../../packages/jdf-core/src/manifest.ts
10
+ var JDFX_MANIFEST_VERSION = "1.0.0";
11
+ var JDFX_DOCUMENT_PATH = "document.json";
12
+ var JDFX_MANIFEST_PATH = "manifest.json";
13
+ var JDFX_ASSET_DIR = "assets";
14
+
15
+ // src/commands/validate.ts
16
+ var __dirname$1 = path2.dirname(fileURLToPath(import.meta.url));
9
17
  function resolveSchemaPath() {
10
- const bundled = path.resolve(__dirname$1, "jdf-schema.json");
18
+ const bundled = path2.resolve(__dirname$1, "jdf-schema.json");
11
19
  if (fs.existsSync(bundled)) return bundled;
12
- const dev = path.resolve(__dirname$1, "../../../../spec/jdf-schema.json");
20
+ const dev = path2.resolve(__dirname$1, "../../../../spec/jdf-schema.json");
13
21
  return dev;
14
22
  }
15
23
  var SCHEMA_PATH = resolveSchemaPath();
24
+ async function loadDocument(filePath) {
25
+ if (filePath.toLowerCase().endsWith(".jdfx")) {
26
+ const zip = await JSZip.loadAsync(fs.readFileSync(filePath));
27
+ const docFile = zip.file(JDFX_DOCUMENT_PATH);
28
+ if (!docFile) {
29
+ console.error(`\u2717 Bundle missing ${JDFX_DOCUMENT_PATH}`);
30
+ return null;
31
+ }
32
+ const doc = JSON.parse(await docFile.async("string"));
33
+ let manifest = null;
34
+ const mf = zip.file(JDFX_MANIFEST_PATH);
35
+ if (mf) {
36
+ try {
37
+ manifest = JSON.parse(await mf.async("string"));
38
+ } catch {
39
+ }
40
+ }
41
+ const assetCount = Object.values(zip.files).filter((f) => !f.dir && f.name.startsWith("assets/")).length;
42
+ return { doc, bundle: { manifest, assetCount } };
43
+ }
44
+ return { doc: JSON.parse(fs.readFileSync(filePath, "utf-8")) };
45
+ }
16
46
  async function validate(file) {
17
- const filePath = path.resolve(file);
47
+ const filePath = path2.resolve(file);
18
48
  if (!fs.existsSync(filePath)) {
19
49
  console.error(`File not found: ${filePath}`);
20
50
  return false;
21
51
  }
22
- let doc;
52
+ let loaded;
23
53
  try {
24
- doc = JSON.parse(fs.readFileSync(filePath, "utf-8"));
54
+ loaded = await loadDocument(filePath);
25
55
  } catch (e) {
26
- console.error(`Invalid JSON: ${e.message}`);
56
+ console.error(`Invalid file: ${e.message}`);
27
57
  return false;
28
58
  }
59
+ if (!loaded) return false;
60
+ const { doc, bundle } = loaded;
29
61
  if (!fs.existsSync(SCHEMA_PATH)) {
30
62
  console.error(`Schema not found at ${SCHEMA_PATH}`);
31
63
  return false;
@@ -39,33 +71,161 @@ async function validate(file) {
39
71
  const d = doc;
40
72
  const pageCount = Array.isArray(d.pages) ? d.pages.length : 0;
41
73
  const elCount = Array.isArray(d.pages) ? d.pages.reduce((acc, p) => acc + (Array.isArray(p?.elements) ? p.elements.length : 0), 0) : 0;
42
- console.log(`\u2713 Valid: ${path.basename(filePath)}`);
43
- console.log(` Format: ${d.$jdf}`);
74
+ console.log(`\u2713 Valid: ${path2.basename(filePath)}`);
75
+ console.log(` Format: ${d.$jdf}${bundle ? " (jdfx bundle)" : ""}`);
44
76
  console.log(` Title: ${d.meta?.title}`);
45
77
  console.log(` Pages: ${pageCount}`);
46
78
  console.log(` Elements: ${elCount}`);
79
+ if (bundle) {
80
+ console.log(` Assets: ${bundle.assetCount}`);
81
+ if (bundle.manifest?.generator) {
82
+ console.log(` Generator: ${bundle.manifest.generator}`);
83
+ }
84
+ }
47
85
  return true;
48
86
  }
49
- console.error(`\u2717 Invalid: ${path.basename(filePath)}`);
87
+ console.error(`\u2717 Invalid: ${path2.basename(filePath)}`);
50
88
  for (const err of validateFn.errors || []) {
51
89
  const loc = err.instancePath || "(root)";
52
90
  console.error(` ${loc} \u2014 ${err.message}`);
53
91
  }
54
92
  return false;
55
93
  }
94
+ var GENERATOR = "@uurtech/jdf-cli";
95
+ function decodeBase64(s) {
96
+ return Buffer.from(s, "base64");
97
+ }
98
+ function extractAssets(doc) {
99
+ const assets = [];
100
+ const cloned = JSON.parse(JSON.stringify(doc));
101
+ let counter = 0;
102
+ function walk(els) {
103
+ if (!els) return;
104
+ for (const el of els) {
105
+ if (el?.type === "image" && typeof el.src === "string" && el.src.startsWith("data:")) {
106
+ const m = el.src.match(/^data:([^;]+);base64,(.*)$/);
107
+ if (m) {
108
+ counter++;
109
+ const id = `asset-${counter}`;
110
+ const mimeType = m[1];
111
+ const ext = mimeType.split("/")[1]?.replace("jpeg", "jpg") || "bin";
112
+ assets.push({ id, bytes: decodeBase64(m[2]), mimeType, ext });
113
+ delete el.src;
114
+ el.resource = id;
115
+ }
116
+ }
117
+ if (el?.elements) walk(el.elements);
118
+ if (el?.children) walk(el.children);
119
+ }
120
+ }
121
+ for (const page of cloned.pages || []) walk(page.elements);
122
+ if (cloned.resources?.images) {
123
+ for (const [key, res] of Object.entries(cloned.resources.images)) {
124
+ if (!res || typeof res !== "object" || !("data" in res) || !res.data) continue;
125
+ const data = String(res.data);
126
+ const m = data.match(/^data:([^;]+);base64,(.*)$/);
127
+ const b64 = m ? m[2] : data;
128
+ const mimeType = m ? m[1] : res.mimeType || "image/png";
129
+ const ext = mimeType.split("/")[1]?.replace("jpeg", "jpg") || "bin";
130
+ counter++;
131
+ const id = key || `asset-${counter}`;
132
+ assets.push({ id, bytes: decodeBase64(b64), mimeType, ext });
133
+ }
134
+ cloned.resources.images = {};
135
+ }
136
+ return { doc: cloned, assets };
137
+ }
138
+ async function packJdfx(doc) {
139
+ const { doc: rewritten, assets } = extractAssets(doc);
140
+ const zip = new JSZip();
141
+ const assetEntries = assets.map((a) => {
142
+ const p = `${JDFX_ASSET_DIR}/${a.id}.${a.ext}`;
143
+ zip.file(p, a.bytes);
144
+ return { id: a.id, path: p, mimeType: a.mimeType, size: a.bytes.length };
145
+ });
146
+ const now = (/* @__PURE__ */ new Date()).toISOString();
147
+ const manifest = {
148
+ format: "jdfx",
149
+ version: JDFX_MANIFEST_VERSION,
150
+ document: JDFX_DOCUMENT_PATH,
151
+ created: now,
152
+ modified: now,
153
+ generator: GENERATOR,
154
+ assets: assetEntries
155
+ };
156
+ zip.file(JDFX_DOCUMENT_PATH, JSON.stringify(rewritten, null, 2));
157
+ zip.file(JDFX_MANIFEST_PATH, JSON.stringify(manifest, null, 2));
158
+ const bytes = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", compressionOptions: { level: 6 } });
159
+ return { bytes, manifest };
160
+ }
161
+ function shouldUseJdfx(doc) {
162
+ const images = doc.resources?.images ?? {};
163
+ for (const v of Object.values(images)) {
164
+ if (v && typeof v === "object" && "data" in v && v.data) return true;
165
+ }
166
+ function walk(els) {
167
+ if (!els) return false;
168
+ for (const el of els) {
169
+ if (el?.type === "image" && typeof el.src === "string" && el.src.startsWith("data:")) return true;
170
+ if (el?.elements && walk(el.elements)) return true;
171
+ if (el?.children && walk(el.children)) return true;
172
+ }
173
+ return false;
174
+ }
175
+ for (const page of doc.pages || []) {
176
+ if (walk(page.elements)) return true;
177
+ }
178
+ return false;
179
+ }
180
+
181
+ // src/commands/import-md.ts
56
182
  async function importMarkdown(inputPath, outputPath) {
57
- const input = path.resolve(inputPath);
58
- const output = outputPath ? path.resolve(outputPath) : input.replace(/\.(md|markdown)$/i, ".jdf");
183
+ const input = path2.resolve(inputPath);
59
184
  console.log(`Importing: ${input}`);
60
- console.log(`Output: ${output}`);
61
185
  const content = fs.readFileSync(input, "utf-8");
62
- const doc = convertMarkdownToJdf(content, path.basename(input, path.extname(input)));
63
- fs.writeFileSync(output, JSON.stringify(doc, null, 2));
64
- console.log(`
186
+ const doc = convertMarkdownToJdf(content, path2.basename(input, path2.extname(input)), path2.dirname(input));
187
+ let output;
188
+ if (outputPath) {
189
+ output = path2.resolve(outputPath);
190
+ } else {
191
+ const stem = input.replace(/\.(md|markdown)$/i, "");
192
+ output = stem + (shouldUseJdfx(doc) ? ".jdfx" : ".jdf");
193
+ }
194
+ console.log(`Output: ${output}`);
195
+ if (output.toLowerCase().endsWith(".jdfx")) {
196
+ const { bytes, manifest } = await packJdfx(doc);
197
+ fs.writeFileSync(output, bytes);
198
+ console.log(`
199
+ Done! Created ${doc.pages.length} page(s), ${manifest.assets.length} asset(s) bundled`);
200
+ } else {
201
+ fs.writeFileSync(output, JSON.stringify(doc, null, 2));
202
+ console.log(`
65
203
  Done! Created ${doc.pages.length} page(s)`);
204
+ }
66
205
  console.log(`Open with: open -a "JDF Reader" "${output}"`);
67
206
  }
68
- function convertMarkdownToJdf(md, title) {
207
+ var MIME_BY_EXT2 = {
208
+ png: "image/png",
209
+ jpg: "image/jpeg",
210
+ jpeg: "image/jpeg",
211
+ gif: "image/gif",
212
+ webp: "image/webp",
213
+ svg: "image/svg+xml",
214
+ bmp: "image/bmp"
215
+ };
216
+ function resolveImageSrc(src, baseDir) {
217
+ if (/^(https?:|data:|file:)/i.test(src)) return src;
218
+ const abs = path2.isAbsolute(src) ? src : path2.resolve(baseDir, src);
219
+ try {
220
+ const bytes = fs.readFileSync(abs);
221
+ const ext = path2.extname(abs).slice(1).toLowerCase();
222
+ const mime = MIME_BY_EXT2[ext] || "application/octet-stream";
223
+ return `data:${mime};base64,${bytes.toString("base64")}`;
224
+ } catch {
225
+ return src;
226
+ }
227
+ }
228
+ function convertMarkdownToJdf(md, title, baseDir = process.cwd()) {
69
229
  const maxY = 247;
70
230
  const contentWidth = 166;
71
231
  const pages = [];
@@ -105,6 +265,30 @@ function convertMarkdownToJdf(md, title) {
105
265
  i++;
106
266
  continue;
107
267
  }
268
+ const imageMatch = line.match(/^\s*!\[([^\]]*)\]\(\s*([^)\s]+)[^)]*\)\s*$/);
269
+ if (imageMatch) {
270
+ const alt = imageMatch[1];
271
+ const src = resolveImageSrc(imageMatch[2], baseDir);
272
+ const height2 = 60;
273
+ if (y + height2 > maxY) {
274
+ pages.push({ id: `page-${pageNum}`, elements });
275
+ elements = [];
276
+ y = 5;
277
+ pageNum++;
278
+ }
279
+ elements.push({
280
+ type: "image",
281
+ src,
282
+ alt,
283
+ position: { x: 0, y },
284
+ width: contentWidth,
285
+ height: height2,
286
+ fit: "contain"
287
+ });
288
+ y += height2 + 4;
289
+ i++;
290
+ continue;
291
+ }
108
292
  if (line.match(/^\s*[-*+]\s/)) {
109
293
  const items = [];
110
294
  while (i < lines.length && lines[i].match(/^\s*[-*+]\s/)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uurtech/jdf-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Command-line tool for the JDF (JSON Document Format) — validate and convert documents.",
5
5
  "license": "MIT",
6
6
  "author": "Ugur Kazdal",
@@ -45,7 +45,8 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "ajv": "^8.17.1",
48
- "ajv-formats": "^3.0.1"
48
+ "ajv-formats": "^3.0.1",
49
+ "jszip": "^3.10.1"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@jdf/core": "workspace:*",