@qcplay/cli 1.0.9 → 1.0.10

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/bin/qcplay.js CHANGED
@@ -9,7 +9,7 @@ import path from "path";
9
9
  import readline from "readline";
10
10
  import { fileURLToPath, pathToFileURL } from "url";
11
11
 
12
- import { buildOfficialArticleMarkdown, importWechatArticle } from "../lib/wechat-article.js";
12
+ import { buildOfficialArticleMarkdown, importWechatArticle, normalizeArticleColor } from "../lib/wechat-article.js";
13
13
 
14
14
  const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = path.dirname(__filename);
@@ -1150,6 +1150,15 @@ function applyInlineMarkdown(text) {
1150
1150
  return storeToken(`<code>${escapeHtml(code)}</code>`);
1151
1151
  });
1152
1152
 
1153
+ output = output.replace(/\{\{qc-color:([^}]+)\}\}([\s\S]*?)\{\{\/qc-color\}\}/g, (_, rawColor, content) => {
1154
+ const color = normalizeArticleColor(rawColor);
1155
+ if (!color) {
1156
+ return content;
1157
+ }
1158
+ const plainText = content.replace(/\\([\\`*_\[\]])/g, "$1");
1159
+ return storeToken(`<span style="color:${escapeHtml(color)};">${escapeHtml(plainText)}</span>`);
1160
+ });
1161
+
1153
1162
  output = output.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, url) => {
1154
1163
  return storeToken(
1155
1164
  `<img src="${escapeHtml(url.trim())}" alt="${escapeHtml(alt.trim())}" ` +
@@ -1171,7 +1180,7 @@ function applyInlineMarkdown(text) {
1171
1180
  output = output.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>");
1172
1181
  output = output.replace(/(?<!_)_([^_]+)_(?!_)/g, "<em>$1</em>");
1173
1182
 
1174
- for (let index = 0; index < tokens.length; index += 1) {
1183
+ for (let index = tokens.length - 1; index >= 0; index -= 1) {
1175
1184
  output = output.replace(`@@MDTOKEN${index}@@`, tokens[index]);
1176
1185
  }
1177
1186
 
@@ -1478,6 +1487,7 @@ async function importWechatArticleCommand(sourceUrl, file = "article.md") {
1478
1487
  console.log(targetFile);
1479
1488
  console.log(chalk.gray(`标题: ${article.title}`));
1480
1489
  console.log(chalk.gray(`已转存图片: ${article.imageCount} 张`));
1490
+ console.log(chalk.gray(`已保留文字颜色: ${article.colorCount} 处`));
1481
1491
  console.log("");
1482
1492
  console.log("请补充 cate_id 等业务字段,确认内容后执行:");
1483
1493
  console.log("");
@@ -234,9 +234,68 @@ function escapeMarkdownText(value) {
234
234
  return normalizeTextContent(value).replace(/([\\`*_\[\]])/g, "\\$1");
235
235
  }
236
236
 
237
- function markdownForNode($, node, listDepth = 0) {
237
+ const NAMED_TEXT_COLORS = new Set([
238
+ "aqua",
239
+ "black",
240
+ "blue",
241
+ "cyan",
242
+ "fuchsia",
243
+ "gray",
244
+ "green",
245
+ "grey",
246
+ "lime",
247
+ "magenta",
248
+ "maroon",
249
+ "navy",
250
+ "olive",
251
+ "orange",
252
+ "pink",
253
+ "purple",
254
+ "red",
255
+ "silver",
256
+ "teal",
257
+ "transparent",
258
+ "white",
259
+ "yellow"
260
+ ]);
261
+
262
+ export function normalizeArticleColor(value) {
263
+ const color = String(value || "")
264
+ .trim()
265
+ .replace(/\s*!important\s*$/i, "")
266
+ .trim()
267
+ .toLowerCase();
268
+ if (/^#[0-9a-f]{3,4}(?:[0-9a-f]{3,4})?$/.test(color)) {
269
+ return color;
270
+ }
271
+ if (/^rgba?\(\s*(?:\d{1,3}%?\s*,\s*){2}\d{1,3}%?(?:\s*,\s*(?:0|1|0?\.\d+|\d{1,3}%))?\s*\)$/.test(color)) {
272
+ return color.replace(/\s+/g, " ");
273
+ }
274
+ return NAMED_TEXT_COLORS.has(color) ? color : "";
275
+ }
276
+
277
+ function elementTextColor(element) {
278
+ const style = [element.attr("style"), element.attr("data-style")].filter(Boolean).join(";");
279
+ let color = "";
280
+ for (const match of style.matchAll(/(?:^|;)\s*(?:color|-webkit-text-fill-color)\s*:\s*([^;]+)/gi)) {
281
+ color = normalizeArticleColor(match[1]) || color;
282
+ }
283
+ return (
284
+ color ||
285
+ normalizeArticleColor(element.attr("color")) ||
286
+ normalizeArticleColor(element.attr("data-color"))
287
+ );
288
+ }
289
+
290
+ function markdownForNode($, node, listDepth = 0, inheritedColor = "", context = { colorCount: 0, colors: new Set() }) {
238
291
  if (node.type === "text") {
239
- return escapeMarkdownText(node.data);
292
+ const value = escapeMarkdownText(node.data);
293
+ if (!inheritedColor || !value.trim()) {
294
+ return value;
295
+ }
296
+ context.colorCount += 1;
297
+ context.colors.add(inheritedColor);
298
+ return `{{qc-color:${inheritedColor}}}${value}{{/qc-color}}`;
240
299
  }
241
300
  if (node.type !== "tag") {
242
301
  return "";
@@ -244,7 +303,13 @@ function markdownForNode($, node, listDepth = 0) {
244
303
 
245
304
  const element = $(node);
246
305
  const tag = String(node.tagName || node.name || "").toLowerCase();
247
- const children = () => element.contents().toArray().map(child => markdownForNode($, child, listDepth)).join("");
306
+ const textColor = elementTextColor(element) || inheritedColor;
307
+ const children = () =>
308
+ element
309
+ .contents()
310
+ .toArray()
311
+ .map(child => markdownForNode($, child, listDepth, textColor, context))
312
+ .join("");
248
313
 
249
314
  if (["script", "style", "noscript", "iframe", "video", "audio", "canvas", "svg", "form", "button", "input"].includes(tag)) {
250
315
  return "";
@@ -297,12 +362,7 @@ function markdownForNode($, node, listDepth = 0) {
297
362
  if (tag === "ul" || tag === "ol") {
298
363
  const ordered = tag === "ol";
299
364
  const items = element.children("li").toArray().map((item, index) => {
300
- const value = $(item)
301
- .contents()
302
- .toArray()
303
- .map(child => markdownForNode($, child, listDepth + 1))
304
- .join("")
305
- .trim();
365
+ const value = markdownForNode($, item, listDepth + 1, textColor, context).trim();
306
366
  const prefix = ordered ? `${index + 1}.` : "-";
307
367
  return `${" ".repeat(listDepth)}${prefix} ${value}`;
308
368
  });
@@ -411,8 +471,13 @@ export async function convertWechatHtml(html, pageUrl, options = {}) {
411
471
  $(element).attr("data-qcplay-src", uploaded.get(key));
412
472
  });
413
473
 
474
+ const colorContext = { colorCount: 0, colors: new Set() };
414
475
  const markdown = normalizeMarkdown(
415
- content.contents().toArray().map(node => markdownForNode($, node)).join("")
476
+ content
477
+ .contents()
478
+ .toArray()
479
+ .map(node => markdownForNode($, node, 0, "", colorContext))
480
+ .join("")
416
481
  );
417
482
  if (!markdown) {
418
483
  throw new Error("微信文章正文转换后为空");
@@ -426,6 +491,8 @@ export async function convertWechatHtml(html, pageUrl, options = {}) {
426
491
  thumbnail: coverSource ? uploaded.get(assertWechatImageUrl(coverSource, sourceUrl).toString()) || "" : uploaded.values().next().value || "",
427
492
  markdown,
428
493
  imageCount: uploaded.size,
494
+ colorCount: colorContext.colorCount,
495
+ colors: [...colorContext.colors],
429
496
  sourceUrl: sourceUrl.toString()
430
497
  };
431
498
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qcplay/cli",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "QCPlay CLI",
5
5
  "type": "module",
6
6
  "bin": {