czon 0.9.7 → 0.9.9

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.
@@ -16,6 +16,7 @@ const path_1 = __importDefault(require("path"));
16
16
  const findEntries_1 = require("../findEntries");
17
17
  const paths_1 = require("../paths");
18
18
  const isExists_1 = require("../utils/isExists");
19
+ const isFile_1 = require("../utils/isFile");
19
20
  /**
20
21
  * 递归遍历 marked token 树,收集所有真实的 link 和 image token。
21
22
  * 自动跳过 code(代码块)和 codespan(行内代码)中的内容。
@@ -192,7 +193,9 @@ async function checkLinks() {
192
193
  }
193
194
  // 再检查文件系统
194
195
  const resolvedFullPath = path_1.default.join(paths_1.INPUT_DIR, resolvedRelative);
195
- if (!(await (0, isExists_1.isExists)(resolvedFullPath))) {
196
+ const targetExists = await (0, isExists_1.isExists)(resolvedFullPath);
197
+ const targetIsFile = targetExists ? await (0, isFile_1.isFile)(resolvedFullPath) : false;
198
+ if (!targetExists || !targetIsFile) {
196
199
  // 通过 basename 模糊匹配,生成候选建议(最多 3 个)
197
200
  const suggestions = [];
198
201
  const targetBasename = path_1.default.basename(hrefWithoutHash);
@@ -209,7 +212,7 @@ async function checkLinks() {
209
212
  raw: link.raw,
210
213
  href: link.href,
211
214
  type: 'dead-link',
212
- message: '目标文件不存在',
215
+ message: targetExists ? '目标是目录,不是文件' : '目标文件不存在',
213
216
  suggestions,
214
217
  });
215
218
  }
@@ -9,6 +9,8 @@ const path_1 = __importDefault(require("path"));
9
9
  const findEntries_1 = require("../findEntries");
10
10
  const metadata_1 = require("../metadata");
11
11
  const paths_1 = require("../paths");
12
+ const isExists_1 = require("../utils/isExists");
13
+ const isFile_1 = require("../utils/isFile");
12
14
  const extractLinksFromMarkdown = (content) => {
13
15
  const linkRegex = /\[.*?\]\((.*?)\)/g;
14
16
  const links = [];
@@ -43,11 +45,14 @@ async function scanSourceFiles() {
43
45
  if (isVisited.has(fullPath))
44
46
  continue;
45
47
  isVisited.add(fullPath);
46
- const isExists = await (0, promises_1.access)(fullPath).then(() => true, () => false);
47
- if (!isExists) {
48
+ if (!(await (0, isExists_1.isExists)(fullPath))) {
48
49
  console.warn(`⚠️ File does not exist: ${fullPath}, skipping.`);
49
50
  continue;
50
51
  }
52
+ if (!(await (0, isFile_1.isFile)(fullPath))) {
53
+ console.warn(`⚠️ Path is not a file: ${fullPath}, skipping.`);
54
+ continue;
55
+ }
51
56
  const contentBuffer = await (0, promises_1.readFile)(fullPath);
52
57
  paths.add(relativePath);
53
58
  let meta = metadata_1.MetaData.files.find(f => f.path === relativePath);
@@ -65,10 +70,21 @@ async function scanSourceFiles() {
65
70
  for (const link of links) {
66
71
  if (URL.canParse(link))
67
72
  continue;
68
- const resolvedPath = path_1.default.resolve(path_1.default.dirname(fullPath), link);
69
- const relativePath = path_1.default.relative(paths_1.INPUT_DIR, resolvedPath);
70
- if (!isVisited.has(relativePath)) {
71
- queue.push(relativePath);
73
+ const hrefWithoutHash = link.split('#')[0];
74
+ const hrefWithoutQuery = hrefWithoutHash.split('?')[0];
75
+ if (!hrefWithoutQuery)
76
+ continue;
77
+ const resolvedPath = path_1.default.resolve(path_1.default.dirname(fullPath), hrefWithoutQuery);
78
+ if ((await (0, isExists_1.isExists)(resolvedPath)) && !(await (0, isFile_1.isFile)(resolvedPath))) {
79
+ console.warn(`⚠️ Link target is a directory: ${link} in ${relativePath}, skipping.`);
80
+ continue;
81
+ }
82
+ const resolvedRelativePath = path_1.default.relative(paths_1.INPUT_DIR, resolvedPath);
83
+ if (resolvedRelativePath.startsWith('..') || path_1.default.isAbsolute(resolvedRelativePath))
84
+ continue;
85
+ const resolvedFullPath = path_1.default.join(paths_1.INPUT_DIR, resolvedRelativePath);
86
+ if (!isVisited.has(resolvedFullPath)) {
87
+ queue.push(resolvedRelativePath);
72
88
  }
73
89
  }
74
90
  }
@@ -90,20 +90,36 @@ const ContentPage = props => {
90
90
  react_1.default.createElement("button", { className: "share-float-btn", id: "share-float-btn" }, "Share"),
91
91
  react_1.default.createElement("div", { className: "share-modal-overlay", id: "share-modal-overlay" },
92
92
  react_1.default.createElement("div", { className: "share-modal" },
93
- react_1.default.createElement("canvas", { id: "share-canvas" }),
93
+ react_1.default.createElement("img", { className: "share-preview", id: "share-preview", alt: "Share preview", src: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" }),
94
94
  react_1.default.createElement("div", { className: "share-modal-actions" },
95
95
  react_1.default.createElement("button", { className: "share-download-btn", id: "share-download-btn" }, "Save Image"),
96
96
  react_1.default.createElement("button", { className: "share-close-btn", id: "share-close-btn" }, "Close")))),
97
+ react_1.default.createElement("div", { className: "share-card", id: "share-card" },
98
+ react_1.default.createElement("div", { className: "share-card-header" },
99
+ react_1.default.createElement("div", { className: "share-card-header-left" },
100
+ react_1.default.createElement("div", { className: "share-card-site", id: "share-card-site" }),
101
+ react_1.default.createElement("div", { className: "share-card-title", id: "share-card-title" })),
102
+ react_1.default.createElement("div", { className: "share-card-qr" },
103
+ react_1.default.createElement("canvas", { id: "share-qr-canvas", width: "64", height: "64" }),
104
+ react_1.default.createElement("span", { className: "share-card-qr-hint" }, "Scan to read"))),
105
+ react_1.default.createElement("div", { className: "share-card-divider" }),
106
+ react_1.default.createElement("div", { className: "share-card-body", id: "share-card-body" })),
97
107
  react_1.default.createElement("script", { id: "qrcode-lib", src: "https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js", defer: true }),
108
+ react_1.default.createElement("script", { id: "html2canvas-lib", src: "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js", defer: true }),
98
109
  react_1.default.createElement("script", { dangerouslySetInnerHTML: {
99
110
  __html: `
100
111
  (function() {
101
112
  var floatBtn = document.getElementById('share-float-btn');
102
113
  var overlay = document.getElementById('share-modal-overlay');
103
- var canvas = document.getElementById('share-canvas');
114
+ var preview = document.getElementById('share-preview');
104
115
  var downloadBtn = document.getElementById('share-download-btn');
105
116
  var closeBtn = document.getElementById('share-close-btn');
106
- var selectedText = '';
117
+ var shareCard = document.getElementById('share-card');
118
+ var cardSite = document.getElementById('share-card-site');
119
+ var cardTitle = document.getElementById('share-card-title');
120
+ var cardBody = document.getElementById('share-card-body');
121
+ var qrCanvas = document.getElementById('share-qr-canvas');
122
+ var savedRange = null;
107
123
  var articleTitle = ${JSON.stringify(title)};
108
124
  var siteName = ${JSON.stringify(props.ctx.site.options?.site?.title || 'CZON')};
109
125
 
@@ -125,25 +141,18 @@ const ContentPage = props => {
125
141
  floatBtn.style.display = 'none';
126
142
  return;
127
143
  }
128
- selectedText = text;
144
+ savedRange = range.cloneRange();
129
145
  var rect = range.getBoundingClientRect();
130
146
  floatBtn.style.display = 'block';
131
- floatBtn.style.top = (window.scrollY + rect.top - 36) + 'px';
147
+ floatBtn.style.top = (window.scrollY + rect.bottom + 6) + 'px';
132
148
  floatBtn.style.left = (window.scrollX + rect.left + rect.width / 2 - 30) + 'px';
133
149
  });
134
150
 
135
- // Hide float button on click elsewhere
136
- document.addEventListener('mousedown', function(e) {
137
- if (e.target === floatBtn) return;
138
- // Let selectionchange handle hiding
139
- });
140
-
141
151
  floatBtn.addEventListener('click', function(e) {
142
152
  e.preventDefault();
143
153
  e.stopPropagation();
144
- if (!selectedText) return;
145
- renderShareCard(selectedText);
146
- overlay.classList.add('active');
154
+ if (!savedRange) return;
155
+ renderShareCard(savedRange);
147
156
  floatBtn.style.display = 'none';
148
157
  });
149
158
 
@@ -155,159 +164,82 @@ const ContentPage = props => {
155
164
  });
156
165
 
157
166
  downloadBtn.addEventListener('click', function() {
158
- canvas.toBlob(function(blob) {
159
- var url = URL.createObjectURL(blob);
160
- var a = document.createElement('a');
161
- a.href = url;
162
- a.download = 'share.png';
163
- a.click();
164
- URL.revokeObjectURL(url);
165
- }, 'image/png');
167
+ var a = document.createElement('a');
168
+ a.href = preview.src;
169
+ a.download = 'share.png';
170
+ a.click();
166
171
  });
167
172
 
168
- function wrapText(ctx, text, maxWidth, lineHeight) {
169
- var lines = [];
170
- var paragraphs = text.split('\\n');
171
- for (var p = 0; p < paragraphs.length; p++) {
172
- var words = paragraphs[p];
173
- var line = '';
174
- for (var i = 0; i < words.length; i++) {
175
- var testLine = line + words[i];
176
- var metrics = ctx.measureText(testLine);
177
- if (metrics.width > maxWidth && line.length > 0) {
178
- lines.push(line);
179
- line = words[i];
180
- } else {
181
- line = testLine;
173
+ function renderQR() {
174
+ if (typeof qrcode === 'undefined') return;
175
+ var size = 64;
176
+ var qr = qrcode(0, 'M');
177
+ qr.addData(window.location.href);
178
+ qr.make();
179
+ var moduleCount = qr.getModuleCount();
180
+ var cellSize = size / moduleCount;
181
+ var ctx = qrCanvas.getContext('2d');
182
+ ctx.clearRect(0, 0, size, size);
183
+ ctx.fillStyle = '#1a1a1a';
184
+ for (var r = 0; r < moduleCount; r++) {
185
+ for (var c = 0; c < moduleCount; c++) {
186
+ if (qr.isDark(r, c)) {
187
+ ctx.fillRect(c * cellSize, r * cellSize, cellSize + 0.5, cellSize + 0.5);
182
188
  }
183
189
  }
184
- if (line) lines.push(line);
185
- if (p < paragraphs.length - 1) lines.push('');
186
190
  }
187
- return lines;
188
191
  }
189
192
 
190
- function renderShareCard(text) {
191
- var dpr = window.devicePixelRatio || 1;
192
- var W = 800;
193
- var pad = 48;
194
- var contentW = W - pad * 2;
195
- var ctx = canvas.getContext('2d');
196
-
197
- // Pre-calculate heights
198
- ctx.font = 'bold 28px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
199
- var titleLines = wrapText(ctx, articleTitle, contentW, 36);
200
- var titleH = titleLines.length * 36;
201
-
202
- ctx.font = '20px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
203
- var maxTextLen = 300;
204
- var displayText = text.length > maxTextLen ? text.slice(0, maxTextLen) + '...' : text;
205
- var textLines = wrapText(ctx, displayText, contentW - 32, 30);
206
- var textH = textLines.length * 30;
193
+ function renderShareCard(range) {
194
+ // Populate card content
195
+ cardSite.textContent = siteName;
196
+ cardTitle.textContent = articleTitle;
207
197
 
208
- var qrSize = 120;
209
- var siteNameH = 40;
210
- var separatorGap = 24;
211
- var quoteTopPad = 24;
212
- var quoteBottomPad = 24;
213
- var qrTopPad = 32;
214
- var qrBottomPad = 16;
215
- var bottomPad = 32;
198
+ // Clone selected DOM fragment with rich formatting
199
+ var fragment = range.cloneContents();
200
+ cardBody.innerHTML = '';
201
+ cardBody.appendChild(fragment);
216
202
 
217
- var H = pad + siteNameH + titleH + separatorGap * 2 + quoteTopPad + textH + quoteBottomPad + qrTopPad + qrSize + qrBottomPad + 20 + bottomPad;
203
+ // Copy computed styles for elements that need them (e.g. KaTeX)
204
+ var allStyles = document.querySelectorAll('style, link[rel="stylesheet"]');
205
+ var styleClones = [];
206
+ allStyles.forEach(function(s) {
207
+ styleClones.push(s.cloneNode(true));
208
+ });
218
209
 
219
- canvas.width = W * dpr;
220
- canvas.height = H * dpr;
221
- canvas.style.width = W + 'px';
222
- canvas.style.height = H + 'px';
223
- ctx.scale(dpr, dpr);
210
+ // Render QR code
211
+ renderQR();
224
212
 
225
- // Background
226
- ctx.fillStyle = '#ffffff';
227
- ctx.beginPath();
228
- ctx.roundRect(0, 0, W, H, 12);
229
- ctx.fill();
230
-
231
- var y = pad;
232
-
233
- // Site name
234
- ctx.fillStyle = '#999999';
235
- ctx.font = '16px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
236
- ctx.fillText(siteName, pad, y + 16);
237
- y += siteNameH;
238
-
239
- // Title
240
- ctx.fillStyle = '#1a1a1a';
241
- ctx.font = 'bold 28px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
242
- for (var i = 0; i < titleLines.length; i++) {
243
- ctx.fillText(titleLines[i], pad, y + 28);
244
- y += 36;
245
- }
246
- y += separatorGap;
213
+ // Show card for html2canvas to capture
214
+ shareCard.classList.add('rendering');
247
215
 
248
- // Separator
249
- ctx.strokeStyle = '#e5e5e5';
250
- ctx.lineWidth = 1;
251
- ctx.beginPath();
252
- ctx.moveTo(pad, y);
253
- ctx.lineTo(W - pad, y);
254
- ctx.stroke();
255
- y += separatorGap;
256
-
257
- // Quote block background
258
- var quoteBlockY = y;
259
- var quoteBlockH = quoteTopPad + textH + quoteBottomPad;
260
- ctx.fillStyle = '#f8f9fa';
261
- ctx.beginPath();
262
- ctx.roundRect(pad, quoteBlockY, contentW, quoteBlockH, 8);
263
- ctx.fill();
264
-
265
- // Quote accent bar
266
- ctx.fillStyle = '#007bff';
267
- ctx.beginPath();
268
- ctx.roundRect(pad, quoteBlockY, 4, quoteBlockH, 2);
269
- ctx.fill();
270
-
271
- // Quote text
272
- y += quoteTopPad;
273
- ctx.fillStyle = '#333333';
274
- ctx.font = '20px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
275
- for (var i = 0; i < textLines.length; i++) {
276
- if (textLines[i] !== '') {
277
- ctx.fillText(textLines[i], pad + 16, y + 20);
216
+ // Wait a frame for layout
217
+ requestAnimationFrame(function() {
218
+ // Enforce minimum 3:4 aspect ratio
219
+ var cardW = shareCard.offsetWidth;
220
+ var cardH = shareCard.offsetHeight;
221
+ var minH = Math.round(cardW * 4 / 3);
222
+ if (cardH < minH) {
223
+ shareCard.style.minHeight = minH + 'px';
278
224
  }
279
- y += 30;
280
- }
281
- y += quoteBottomPad + qrTopPad;
282
-
283
- // QR code
284
- if (typeof qrcode !== 'undefined') {
285
- var qr = qrcode(0, 'M');
286
- qr.addData(window.location.href);
287
- qr.make();
288
- var moduleCount = qr.getModuleCount();
289
- var cellSize = qrSize / moduleCount;
290
- var qrX = W - pad - qrSize;
291
- var qrY = y;
292
225
 
293
- // White background for QR
294
- ctx.fillStyle = '#ffffff';
295
- ctx.fillRect(qrX - 4, qrY - 4, qrSize + 8, qrSize + 8);
226
+ html2canvas(shareCard, {
227
+ scale: 2,
228
+ useCORS: true,
229
+ backgroundColor: '#ffffff',
230
+ logging: false,
231
+ }).then(function(canvas) {
232
+ shareCard.classList.remove('rendering');
233
+ shareCard.style.minHeight = '';
296
234
 
297
- ctx.fillStyle = '#1a1a1a';
298
- for (var r = 0; r < moduleCount; r++) {
299
- for (var c = 0; c < moduleCount; c++) {
300
- if (qr.isDark(r, c)) {
301
- ctx.fillRect(qrX + c * cellSize, qrY + r * cellSize, cellSize + 0.5, cellSize + 0.5);
302
- }
303
- }
304
- }
305
-
306
- // Hint text next to QR
307
- ctx.fillStyle = '#999999';
308
- ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
309
- ctx.fillText('Scan to read the original', pad, y + qrSize / 2 + 5);
310
- }
235
+ preview.src = canvas.toDataURL('image/png');
236
+ overlay.classList.add('active');
237
+ }).catch(function(err) {
238
+ console.error('Share card render error:', err);
239
+ shareCard.classList.remove('rendering');
240
+ shareCard.style.minHeight = '';
241
+ });
242
+ });
311
243
  }
312
244
  })();
313
245
  `,
@@ -410,7 +342,7 @@ const ContentPage = props => {
410
342
 
411
343
  function renderEmblaCarousels() {
412
344
  // Detect image groups, make them carousels automatically
413
- Map.groupBy(document.querySelectorAll('img'), x => x.parentNode).entries().forEach(([container, images]) => {
345
+ Map.groupBy(document.querySelectorAll('.content-body img'), x => x.parentNode).entries().forEach(([container, images]) => {
414
346
  const outer = document.createElement('div');
415
347
  outer.classList.add('embla');
416
348
  container.appendChild(outer);
package/dist/ssg/style.js CHANGED
@@ -424,7 +424,7 @@ html:not(.dark) body {
424
424
  align-items: center;
425
425
  gap: 16px;
426
426
  }
427
- .share-modal canvas {
427
+ .share-modal img.share-preview {
428
428
  max-width: 100%;
429
429
  height: auto;
430
430
  border-radius: 8px;
@@ -449,5 +449,90 @@ html:not(.dark) body {
449
449
  background: var(--tag-bg);
450
450
  color: var(--tag-text);
451
451
  }
452
+
453
+ /* Share card (off-screen DOM for html2canvas capture) */
454
+ .share-card {
455
+ display: none;
456
+ position: fixed;
457
+ left: -9999px;
458
+ top: 0;
459
+ width: 540px;
460
+ min-height: 720px;
461
+ background: #ffffff;
462
+ border-radius: 12px;
463
+ padding: 36px;
464
+ box-sizing: border-box;
465
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
466
+ color: #1a1a1a;
467
+ flex-direction: column;
468
+ pointer-events: none;
469
+ }
470
+ .share-card.rendering {
471
+ display: flex;
472
+ }
473
+ .share-card-header {
474
+ display: flex;
475
+ justify-content: space-between;
476
+ align-items: flex-start;
477
+ gap: 12px;
478
+ margin-bottom: 20px;
479
+ }
480
+ .share-card-header-left {
481
+ flex: 1;
482
+ min-width: 0;
483
+ }
484
+ .share-card-site {
485
+ font-size: 14px;
486
+ color: #999999;
487
+ margin-bottom: 8px;
488
+ }
489
+ .share-card-title {
490
+ font-size: 24px;
491
+ font-weight: 700;
492
+ color: #1a1a1a;
493
+ line-height: 1.3;
494
+ word-wrap: break-word;
495
+ }
496
+ .share-card-qr {
497
+ flex-shrink: 0;
498
+ display: flex;
499
+ flex-direction: column;
500
+ align-items: center;
501
+ }
502
+ .share-card-qr canvas {
503
+ display: block;
504
+ }
505
+ .share-card-qr-hint {
506
+ font-size: 10px;
507
+ color: #bbbbbb;
508
+ margin-top: 4px;
509
+ white-space: nowrap;
510
+ }
511
+ .share-card-divider {
512
+ height: 1px;
513
+ background: #e5e5e5;
514
+ margin-bottom: 20px;
515
+ }
516
+ .share-card-body {
517
+ flex: 1;
518
+ background: #f8f9fa;
519
+ border-radius: 8px;
520
+ padding: 20px;
521
+ font-size: 18px;
522
+ line-height: 1.8;
523
+ color: #333333;
524
+ overflow: hidden;
525
+ }
526
+ .share-card-body img {
527
+ max-width: 100%;
528
+ height: auto;
529
+ }
530
+ .share-card-body pre {
531
+ white-space: pre-wrap;
532
+ word-wrap: break-word;
533
+ }
534
+ .share-card-body strong {
535
+ color: #ff5722;
536
+ }
452
537
  `;
453
538
  //# sourceMappingURL=style.js.map
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isFile = void 0;
4
+ const promises_1 = require("fs/promises");
5
+ /**
6
+ * 检查路径是否为文件
7
+ * @param path 要检查的路径
8
+ * @returns Promise<boolean> 路径是否是文件
9
+ */
10
+ const isFile = async (path) => {
11
+ return (0, promises_1.stat)(path)
12
+ .then(stats => stats.isFile())
13
+ .catch(() => false);
14
+ };
15
+ exports.isFile = isFile;
16
+ //# sourceMappingURL=isFile.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "czon",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "CZON - AI enhanced Markdown content engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",