@qcplay/cli 1.0.15 → 1.0.16

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.
@@ -14,6 +14,10 @@ const RULE_ACTIONS = new Set([
14
14
  "remove_text_block",
15
15
  "replace_text",
16
16
  "remove_image",
17
+ "remove_first_image",
18
+ "remove_last_image",
19
+ "remove_images_between_text",
20
+ "remove_first_video",
17
21
  "replace_image",
18
22
  "remove_image_caption"
19
23
  ]);
@@ -168,7 +172,15 @@ function validateAction(action, location) {
168
172
  const imageFields = new Set(["src", "alt", "title", "caption", "nearby_text"]);
169
173
  const removableImageFields = new Set([...imageFields, "sha256"]);
170
174
  const textFields = new Set(["text"]);
171
- if (action.type === "remove_image_caption") {
175
+ if (["remove_first_image", "remove_last_image", "remove_first_video"].includes(action.type)) {
176
+ if (action.match !== undefined) ruleError(location, `${action.type} 不支持 match`);
177
+ } else if (action.type === "remove_images_between_text") {
178
+ validateMatch(action.match, `${location}.match`, textFields);
179
+ validateMatch(action.end_match, `${location}.end_match`, textFields);
180
+ if (action.match.text === undefined || action.end_match.text === undefined) {
181
+ ruleError(location, "remove_images_between_text 必须提供起止 text");
182
+ }
183
+ } else if (action.type === "remove_image_caption") {
172
184
  validateMatch(action.match || {}, `${location}.match`, imageFields);
173
185
  } else if (action.type === "remove_image" || action.type === "replace_image") {
174
186
  validateMatch(action.match, `${location}.match`, action.type === "remove_image" ? removableImageFields : imageFields);
@@ -343,7 +355,7 @@ function docxImageHashes(buffer) {
343
355
  const commentLength = buffer.readUInt16LE(offset + 32);
344
356
  const localHeaderOffset = buffer.readUInt32LE(offset + 42);
345
357
  const fileName = buffer.subarray(offset + 46, offset + 46 + fileNameLength).toString("utf8");
346
- if (/^word\/media\//i.test(fileName)) {
358
+ if (/^word\/media\/[^/]+$/i.test(fileName)) {
347
359
  if (buffer.readUInt32LE(localHeaderOffset) !== 0x04034b50) throw new Error("DOCX 条目无效");
348
360
  const localNameLength = buffer.readUInt16LE(localHeaderOffset + 26);
349
361
  const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
@@ -368,10 +380,9 @@ function parseRuleText(text, file, imageHashes = []) {
368
380
  const fields = { "规则名称": "name", "项目": "project", "地区": "region", "平台": "platform", "来源": "source", "阶段": "phase" };
369
381
  const ignored = /^(?:#|\/\/|说明[::]?|其他要求[::]?)\s*/;
370
382
  const unsupported = [];
371
- let hasImageHashRule = false;
372
383
  for (const rawLine of trimmed.split(/\r?\n/)) {
373
384
  const line = rawLine.trim().replace(/^(?:[-*•]\s*|\d+[.、]\s*)/, "");
374
- if (!line || ignored.test(line) || /(?:样式全部保留|样式.*保留不变|区分.*b站.*非b站|可以有两篇文章|^文章.*规则(?:修改)?$)/i.test(line)) continue;
385
+ if (!line || ignored.test(line) || /(?:样式全部保留|样式.*保留不变|区分.*b站.*非b站|可以有两篇文章|^文章.*规(?:则)?(?:修改)?$)/i.test(line)) continue;
375
386
  if (line === "最强蜗牛") {
376
387
  rule.project = line;
377
388
  continue;
@@ -412,21 +423,51 @@ function parseRuleText(text, file, imageHashes = []) {
412
423
  }
413
424
  const imageSection = /^(?:b站的内容要删除如下图片|删除图片)\s*[::]?\s*$/i.test(line);
414
425
  if (imageSection) {
415
- if (imageHashes.length > 0 && !hasImageHashRule) {
416
- rules.push({
417
- name: `${rule.name}-B站图片`,
418
- enabled: true,
419
- platform: "bilibili",
420
- phase: "import",
421
- actions: imageHashes.map(sha256 => ({ type: "remove_image", match: { sha256 } }))
422
- });
423
- hasImageHashRule = true;
424
- }
426
+ rule.actions.push(...imageHashes.map(sha256 => ({ type: "remove_image", match: { sha256 } })));
427
+ continue;
428
+ }
429
+ if (/^删除第一张图片\s*[::]?\s*$/i.test(line)) {
430
+ rule.actions.push({ type: "remove_first_image" });
431
+ continue;
432
+ }
433
+ if (/^删除最后一张图片\s*[::]?\s*$/i.test(line)) {
434
+ rule.actions.push({ type: "remove_last_image" });
435
+ continue;
436
+ }
437
+ const imagesBetween = line.match(/^删除(?:从)?\s*(?!图片[::])(.+?)\s*(?:到|至)\s*(.+?)\s*中间的图片$/);
438
+ if (imagesBetween) {
439
+ rule.actions.push({
440
+ type: "remove_images_between_text",
441
+ match: { text: imagesBetween[1].trim(), operator: "contains" },
442
+ end_match: { text: imagesBetween[2].trim(), operator: "contains" }
443
+ });
425
444
  continue;
426
445
  }
427
446
  const image = line.match(/^删除图片[::]\s*(.+)$/);
428
447
  if (image) {
429
- rule.actions.push({ type: "remove_image", match: { nearby_text: image[1].trim(), operator: "contains" } });
448
+ const instruction = image[1].trim();
449
+ if (/^最后一张图片$/i.test(instruction)) {
450
+ rule.actions.push({ type: "remove_last_image" });
451
+ continue;
452
+ }
453
+ if (/^第一张图片$/i.test(instruction)) {
454
+ rule.actions.push({ type: "remove_first_image" });
455
+ continue;
456
+ }
457
+ const nestedImagesBetween = instruction.match(/^删除(?:从)?\s*(.+?)\s*(?:到|至)\s*(.+?)\s*中间的图片$/);
458
+ if (nestedImagesBetween) {
459
+ rule.actions.push({
460
+ type: "remove_images_between_text",
461
+ match: { text: nestedImagesBetween[1].trim(), operator: "contains" },
462
+ end_match: { text: nestedImagesBetween[2].trim(), operator: "contains" }
463
+ });
464
+ continue;
465
+ }
466
+ rule.actions.push({ type: "remove_image", match: { nearby_text: instruction, operator: "contains" } });
467
+ continue;
468
+ }
469
+ if (/^删除视频[::]\s*第一个视频$/i.test(line)) {
470
+ rule.actions.push({ type: "remove_first_video" });
430
471
  continue;
431
472
  }
432
473
  unsupported.push(rawLine.trim());
@@ -693,6 +734,22 @@ function replaceTextNodes($, match, replacement) {
693
734
  return changed;
694
735
  }
695
736
 
737
+ function documentOrder($) {
738
+ const nodes = [];
739
+ const visit = node => {
740
+ nodes.push(node);
741
+ for (const child of node.children || []) visit(child);
742
+ };
743
+ for (const node of $.root().contents().toArray()) visit(node);
744
+ return nodes;
745
+ }
746
+
747
+ function firstMatchingContentBlock($, match) {
748
+ return $(BLOCK_SELECTOR)
749
+ .toArray()
750
+ .find(node => matchesValue($(node).text().trim(), match.text, match));
751
+ }
752
+
696
753
  function applyAction($, action) {
697
754
  if (action.type === "remove_section_until_next_heading") {
698
755
  let changed = false;
@@ -713,6 +770,44 @@ function applyAction($, action) {
713
770
  if (action.type === "replace_text") {
714
771
  return replaceTextNodes($, action.match, action.replacement);
715
772
  }
773
+ if (action.type === "remove_first_image") {
774
+ const firstImage = $("img").first();
775
+ if (!firstImage.length) return false;
776
+ removeImage($, firstImage);
777
+ return true;
778
+ }
779
+ if (action.type === "remove_last_image") {
780
+ const lastImage = $("img").last();
781
+ if (!lastImage.length) return false;
782
+ removeImage($, lastImage);
783
+ return true;
784
+ }
785
+ if (action.type === "remove_first_video") {
786
+ const firstVideo = $("video,mp-common-videosnap").first();
787
+ if (!firstVideo.length) return false;
788
+ const container = firstVideo.closest("p,figure,div,section").first();
789
+ if (container.length && !container.text().trim() && container.find("video,mp-common-videosnap").length === 1) {
790
+ container.remove();
791
+ } else {
792
+ firstVideo.remove();
793
+ }
794
+ return true;
795
+ }
796
+ if (action.type === "remove_images_between_text") {
797
+ const startNode = firstMatchingContentBlock($, action.match);
798
+ const endNode = firstMatchingContentBlock($, action.end_match);
799
+ if (!startNode || !endNode) return false;
800
+ const nodes = documentOrder($);
801
+ const start = nodes.indexOf(startNode);
802
+ const end = nodes.indexOf(endNode);
803
+ if (start < 0 || end < 0 || start >= end) return false;
804
+ const images = $("img").toArray().filter(node => {
805
+ const position = nodes.indexOf(node);
806
+ return position > start && position < end;
807
+ });
808
+ images.forEach(node => removeImage($, $(node)));
809
+ return images.length > 0;
810
+ }
716
811
  const images = $("img").toArray().filter(node => imageMatches($, $(node), action.match || {}));
717
812
  if (action.type === "remove_image") {
718
813
  images.forEach(node => removeImage($, $(node)));
@@ -756,6 +851,10 @@ export function applyContentRules(article, entry = {}, config = entry.contentRul
756
851
  "remove_text_block",
757
852
  "replace_text",
758
853
  "remove_image",
854
+ "remove_first_image",
855
+ "remove_last_image",
856
+ "remove_images_between_text",
857
+ "remove_first_video",
759
858
  "replace_image",
760
859
  "remove_image_caption"
761
860
  ]);
@@ -767,6 +866,10 @@ export function applyContentRules(article, entry = {}, config = entry.contentRul
767
866
  if (options.phase === "import") {
768
867
  const priority = {
769
868
  remove_image: 0,
869
+ remove_first_image: 0,
870
+ remove_last_image: 0,
871
+ remove_images_between_text: 0,
872
+ remove_first_video: 0,
770
873
  replace_image: 0,
771
874
  remove_image_caption: 0,
772
875
  remove_section_until_next_heading: 1,
@@ -1386,6 +1386,14 @@ export async function convertWechatHtml(html, pageUrl, options = {}) {
1386
1386
  const key = assertWechatImageUrl(rawSource, sourceUrl).toString();
1387
1387
  return uploaded.get(key);
1388
1388
  });
1389
+ const retainedSources = entry.sources.filter(rawSource => {
1390
+ const key = assertWechatImageUrl(rawSource, sourceUrl).toString();
1391
+ return !ignored.has(key);
1392
+ });
1393
+ if (retainedSources.length === 0) {
1394
+ $(entry.element).css("background-image", "none");
1395
+ return;
1396
+ }
1389
1397
  $(entry.element).attr("data-qcplay-background-processed", "1");
1390
1398
  $(entry.element).attr("data-qcplay-background-srcs", urls.filter(Boolean).join("|"));
1391
1399
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qcplay/cli",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "QCPlay CLI",
5
5
  "type": "module",
6
6
  "bin": {