@aiquants/html-to-markdown 0.1.3 → 0.1.4

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.
@@ -20384,6 +20384,123 @@ const rehypeAbsoluteLinks = (options) => {
20384
20384
  });
20385
20385
  };
20386
20386
  };
20387
+ const blockLevelTags = /* @__PURE__ */ new Set([
20388
+ "address",
20389
+ "article",
20390
+ "aside",
20391
+ "blockquote",
20392
+ "br",
20393
+ "details",
20394
+ "dialog",
20395
+ "dd",
20396
+ "div",
20397
+ "dl",
20398
+ "dt",
20399
+ "fieldset",
20400
+ "figcaption",
20401
+ "figure",
20402
+ "footer",
20403
+ "form",
20404
+ // "h1", // # 見出し<span id="custom-id"></span> となってしまうのを回避
20405
+ // "h2", // ## 見出し<span id="custom-id"></span> となってしまうのを回避
20406
+ // "h3", // ### 見出し<span id="custom-id"></span> となってしまうのを回避
20407
+ // "h4", // #### 見出し<span id="custom-id"></span> となってしまうのを回避
20408
+ // "h5", // ##### 見出し<span id="custom-id"></span> となってしまうのを回避
20409
+ // "h6", // ###### 見出し<span id="custom-id"></span> となってしまうのを回避
20410
+ "header",
20411
+ "hgroup",
20412
+ "hr",
20413
+ "li",
20414
+ "main",
20415
+ "nav",
20416
+ "ol",
20417
+ "p",
20418
+ "pre",
20419
+ "section",
20420
+ "table",
20421
+ "ul"
20422
+ ]);
20423
+ function hasBlockElement(node2) {
20424
+ let found = false;
20425
+ visit(node2, "element", (el) => {
20426
+ if (blockLevelTags.has(el.tagName)) {
20427
+ found = true;
20428
+ return EXIT;
20429
+ }
20430
+ });
20431
+ return found;
20432
+ }
20433
+ const slugger = new BananaSlug();
20434
+ const rehypeNamedAnchors = () => (tree) => {
20435
+ visit(tree, "element", (node2, index2, parent) => {
20436
+ if (parent && typeof index2 === "number" && node2.properties && node2.properties.id) {
20437
+ const id = String(node2.properties.id);
20438
+ if (id && ["h1", "h2", "h3", "h4", "h5", "h6"].includes(node2.tagName)) {
20439
+ const customId = slugger.slug(id);
20440
+ node2.children.push({
20441
+ type: "text",
20442
+ value: ` {#${customId}}`
20443
+ });
20444
+ return;
20445
+ }
20446
+ if (node2.tagName === "span" && id && Object.keys(node2.properties || {}).length === 1) {
20447
+ return;
20448
+ }
20449
+ const anchorNode = {
20450
+ type: "element",
20451
+ tagName: "span",
20452
+ properties: { id },
20453
+ children: []
20454
+ };
20455
+ if (hasBlockElement(node2)) {
20456
+ let firstInlineInfo = null;
20457
+ visit(node2, (child, childIndex, childParent) => {
20458
+ if (childParent && typeof childIndex === "number") {
20459
+ if (child.type === "element" && !blockLevelTags.has(child.tagName) || child.type === "text" && child.value.trim() !== "") {
20460
+ firstInlineInfo = {
20461
+ node: node2,
20462
+ parent: childParent,
20463
+ index: childIndex
20464
+ };
20465
+ return EXIT;
20466
+ }
20467
+ }
20468
+ });
20469
+ if (firstInlineInfo) {
20470
+ const inline = firstInlineInfo;
20471
+ inline.parent.children.splice(inline.index + 1, 0, anchorNode);
20472
+ delete node2.properties.id;
20473
+ }
20474
+ return;
20475
+ } else {
20476
+ parent.children.splice(index2 + 1, 0, anchorNode);
20477
+ delete node2.properties.id;
20478
+ return index2 + 1;
20479
+ }
20480
+ }
20481
+ });
20482
+ };
20483
+ const rehypeParagraphWrapper = () => (tree) => {
20484
+ visit(tree, "element", (node2, index2, parent) => {
20485
+ if (parent && typeof index2 === "number") {
20486
+ const nextNode = parent.children[index2 + 1];
20487
+ if (nextNode && nextNode.type === "element" && nextNode.tagName === "span" && nextNode.properties?.id && Object.keys(nextNode.properties).length === 1) {
20488
+ const parentIsBlock = !parent || parent.type === "root" || ["body", "div", "section", "article", "aside", "footer", "header", "nav", "td", "th", "li"].includes(parent.tagName);
20489
+ if (parentIsBlock) {
20490
+ const paragraph2 = {
20491
+ type: "element",
20492
+ tagName: "p",
20493
+ properties: {},
20494
+ children: [node2, nextNode]
20495
+ // 現在のノードと次の span ノードを子として含める
20496
+ };
20497
+ parent.children.splice(index2, 2, paragraph2);
20498
+ return index2 + 1;
20499
+ }
20500
+ }
20501
+ }
20502
+ });
20503
+ };
20387
20504
  const rehypeSanitizeHtml = () => {
20388
20505
  return (tree) => {
20389
20506
  visit(tree, (node2, index2, parent) => {
@@ -20477,7 +20594,7 @@ const rehypeWikipediaFootnotes = () => {
20477
20594
  };
20478
20595
  };
20479
20596
  const handleTableCell = (_state, node2, _parent) => {
20480
- const allowedTags = ["br", "hr", "h1", "h2", "h3", "h4", "h5", "h6", "strong", "em", "b", "i", "u", "s", "strike", "del", "ins", "code", "ul", "ol", "li", "a", "img", "blockquote", "pre", "table", "thead", "tbody", "tfoot", "tr", "th", "td"];
20597
+ const allowedTags = ["br", "hr", "h1", "h2", "h3", "h4", "h5", "h6", "strong", "em", "b", "i", "u", "s", "strike", "del", "ins", "code", "ul", "ol", "li", "a", "img", "blockquote", "pre", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "style"];
20481
20598
  const allowedAttributes = ["style", "class", "id", "colspan", "rowspan", "width", "height", "align", "href", "src", "title", "alt"];
20482
20599
  visit(node2, "element", (elementNode, index2, parent) => {
20483
20600
  if (!parent || typeof index2 !== "number") {
@@ -20535,22 +20652,37 @@ const handleTableCell = (_state, node2, _parent) => {
20535
20652
  };
20536
20653
  const convertHtmlToMarkdown = async (html2, baseUrl, locale) => {
20537
20654
  console.log(getMessage(locale, "convert_start"));
20538
- const slugger = new BananaSlug();
20539
- const file = await unified().use(rehypeParse, { fragment: true }).use(rehypeRaw).use(rehypeSanitizeHtml).use(rehypeSlug).use(rehypeAbsoluteLinks, { baseUrl }).use(rehypeWikipediaFootnotes).use(rehypeRemark, {
20655
+ const slugger2 = new BananaSlug();
20656
+ const file = await unified().use(rehypeParse, { fragment: true }).use(rehypeRaw).use(rehypeSanitizeHtml).use(rehypeWikipediaFootnotes).use(rehypeNamedAnchors).use(rehypeSlug).use(rehypeParagraphWrapper).use(rehypeAbsoluteLinks, { baseUrl }).use(rehypeRemark, {
20540
20657
  handlers: {
20658
+ span(state, node2) {
20659
+ const id = node2.properties?.id;
20660
+ if (id && Object.keys(node2.properties || {}).length === 1) {
20661
+ const children = state.all(node2);
20662
+ if (children.length === 0) {
20663
+ return { type: "html", value: `<span id="${String(id)}"></span>` };
20664
+ }
20665
+ return [
20666
+ { type: "html", value: `<span id="${String(id)}">` },
20667
+ ...children,
20668
+ { type: "html", value: "</span>" }
20669
+ ];
20670
+ }
20671
+ return state.all(node2);
20672
+ },
20541
20673
  a(state, node2) {
20542
20674
  const hasComplexChildren = node2.children.some((child) => child.type === "element" && ["dl", "ul", "ol", "table", "div"].includes(child.tagName));
20543
20675
  if (hasComplexChildren && node2.properties?.href) {
20544
20676
  const linkText2 = String(node2.properties.ariaLabel || "");
20545
20677
  if (linkText2) {
20546
- const url2 = String(node2.properties.href);
20547
- const linkNode = {
20678
+ const url = String(node2.properties.href);
20679
+ const linkNode2 = {
20548
20680
  type: "link",
20549
- url: url2,
20681
+ url,
20550
20682
  children: [{ type: "text", value: linkText2 }]
20551
20683
  };
20552
20684
  const otherContent = state.all(node2);
20553
- return [linkNode, { type: "text", value: "\n" }, ...otherContent];
20685
+ return [linkNode2, { type: "text", value: "\n" }, ...otherContent];
20554
20686
  }
20555
20687
  }
20556
20688
  const extractTextFromHast = (n) => {
@@ -20584,24 +20716,27 @@ const convertHtmlToMarkdown = async (html2, baseUrl, locale) => {
20584
20716
  return false;
20585
20717
  });
20586
20718
  };
20719
+ let linkNode;
20587
20720
  if (containsImage(node2)) {
20588
- return {
20721
+ linkNode = {
20589
20722
  type: "link",
20590
20723
  url: String(node2.properties?.href || ""),
20591
20724
  title: node2.properties?.title ? String(node2.properties.title) : null,
20592
20725
  children: state.all(node2)
20593
20726
  };
20727
+ } else {
20728
+ let url = String(node2.properties?.href || "");
20729
+ if (url.startsWith("#")) {
20730
+ url = `#${slugger2.slug(url.substring(1))}`;
20731
+ }
20732
+ linkNode = {
20733
+ type: "link",
20734
+ url,
20735
+ title: node2.properties?.title ? String(node2.properties.title) : null,
20736
+ children: [{ type: "text", value: linkText }]
20737
+ };
20594
20738
  }
20595
- let url = String(node2.properties?.href || "");
20596
- if (url.startsWith("#")) {
20597
- url = `#${slugger.slug(url.substring(1))}`;
20598
- }
20599
- return {
20600
- type: "link",
20601
- url,
20602
- title: node2.properties?.title ? String(node2.properties.title) : null,
20603
- children: [{ type: "text", value: linkText }]
20604
- };
20739
+ return linkNode;
20605
20740
  },
20606
20741
  td(state, node2, parent) {
20607
20742
  return handleTableCell(state, node2);
@@ -20632,13 +20767,13 @@ const htmlToMarkdown = async (url, options = {}) => {
20632
20767
  const htmlContent = await getHtmlWithPlaywright(url, locale);
20633
20768
  if (!htmlContent) {
20634
20769
  console.error(`No HTML content found for URL: ${url}`);
20635
- return "";
20770
+ return { html: "", markdown: "" };
20636
20771
  }
20637
20772
  const markdown = await convertHtmlToMarkdown(htmlContent, url, locale);
20638
- return markdown;
20773
+ return { html: htmlContent, markdown };
20639
20774
  } catch (error) {
20640
20775
  console.error(`An error occurred during the conversion process for URL: ${url}`, error);
20641
- return "";
20776
+ return { html: "", markdown: "" };
20642
20777
  }
20643
20778
  };
20644
20779
  export {
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- const e=new(require("happy-dom").Window);Object.assign(global,{document:e.document,window:e,self:e}),Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("./core-DUdcm-c8.cjs");exports.getMessage=o.getMessage,exports.htmlToMarkdown=o.htmlToMarkdown;
1
+ const e=new(require("happy-dom").Window);Object.assign(global,{document:e.document,window:e,self:e}),Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("./core-CVMYjtlI.cjs");exports.getMessage=o.getMessage,exports.htmlToMarkdown=o.htmlToMarkdown;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ Object.assign(global, {
5
5
  window,
6
6
  self: window
7
7
  });
8
- import { g, h } from "./core-0q2nkcWH.js";
8
+ import { g, h } from "./core-DOzMLPij.js";
9
9
  export {
10
10
  g as getMessage,
11
11
  h as htmlToMarkdown
package/dist/main.cjs CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- Object.create,Object.defineProperty,Object.getOwnPropertyDescriptor,Object.getOwnPropertyNames,Object.getPrototypeOf,Object.prototype.hasOwnProperty;const e=new(require("happy-dom").Window);Object.assign(global,{document:e.document,window:e,self:e});const t=require("./core-DUdcm-c8.cjs");var r="undefined"!=typeof document?document.currentScript:null;(async()=>{(("undefined"==typeof document?require("url").pathToFileURL(__filename).href:r&&"SCRIPT"===r.tagName.toUpperCase()&&r.src||new URL("main.cjs",document.baseURI).href).endsWith(process.argv[1])||process.argv[1]&&process.argv[1].includes("vite-node"))&&(async()=>{const e=(await import("yargs-parser")).default,r=await import("node:fs"),o=await import("node:path"),c=e(process.argv.slice(2),{string:["locale","output"],alias:{output:["o"]},default:{locale:"en-US"}}),s=c._[0],n=c.locale,a=c.output;s||process.exit(1);try{new URL(s)}catch(i){process.exit(1)}try{const e=await t.htmlToMarkdown(s,{locale:n});let c;if(a){c=o.resolve(process.cwd(),a);const e=o.dirname(c);r.existsSync(e)||r.mkdirSync(e,{recursive:!0})}else{const e=`${s.replace(/https?:\/\//,"").replace(/[^a-zA-Z0-9]/g,"_")}_${Date.now()}.md`,t=o.join(process.cwd(),".outputs","raw");r.existsSync(t)||r.mkdirSync(t,{recursive:!0}),c=o.join(t,e)}r.writeFileSync(c,e)}catch(i){process.exit(1)}})()})();
2
+ Object.create,Object.defineProperty,Object.getOwnPropertyDescriptor,Object.getOwnPropertyNames,Object.getPrototypeOf,Object.prototype.hasOwnProperty;const e=new(require("happy-dom").Window);Object.assign(global,{document:e.document,window:e,self:e});const t=require("./core-CVMYjtlI.cjs");var r="undefined"!=typeof document?document.currentScript:null;(async()=>{(("undefined"==typeof document?require("url").pathToFileURL(__filename).href:r&&"SCRIPT"===r.tagName.toUpperCase()&&r.src||new URL("main.cjs",document.baseURI).href).endsWith(process.argv[1])||process.argv[1]&&process.argv[1].includes("vite-node"))&&(async()=>{const e=(await import("yargs-parser")).default,r=await import("node:fs"),o=await import("node:path"),c=e(process.argv.slice(2),{string:["locale","output"],alias:{output:["o"]},default:{locale:"en-US"}}),s=c._[0],n=c.locale,a=c.output;s||process.exit(1);try{new URL(s)}catch(i){process.exit(1)}try{const{markdown:e,html:c}=await t.htmlToMarkdown(s,{locale:n});let i;if(a){i=o.resolve(process.cwd(),a);const e=o.dirname(i);r.existsSync(e)||r.mkdirSync(e,{recursive:!0})}else{const e=`${s.replace(/https?:\/\//,"").replace(/[^a-zA-Z0-9]/g,"_")}_${Date.now()}.md`,t=o.join(process.cwd(),".outputs","raw");r.existsSync(t)||r.mkdirSync(t,{recursive:!0}),i=o.join(t,e)}r.writeFileSync(i,e);const p=o.parse(i),l=o.join(p.dir,`${p.name}.html`);r.writeFileSync(l,c)}catch(i){process.exit(1)}})()})();
package/dist/main.js CHANGED
@@ -6,7 +6,7 @@ Object.assign(global, {
6
6
  window,
7
7
  self: window
8
8
  });
9
- import { g as getMessage, h as htmlToMarkdown } from "./core-0q2nkcWH.js";
9
+ import { g as getMessage, h as htmlToMarkdown } from "./core-DOzMLPij.js";
10
10
  const runCli = async () => {
11
11
  const yargsParser = (await import("yargs-parser")).default;
12
12
  const fs = await import("node:fs");
@@ -44,7 +44,7 @@ ${getMessage(locale, "cli_start")}`);
44
44
  if (outputFile) {
45
45
  console.info(getMessage(locale, "cli_output", { path: outputFile }));
46
46
  }
47
- const markdown = await htmlToMarkdown(targetUrl, { locale });
47
+ const { markdown, html } = await htmlToMarkdown(targetUrl, { locale });
48
48
  let filePath;
49
49
  if (outputFile) {
50
50
  filePath = path.resolve(process.cwd(), outputFile);
@@ -63,9 +63,13 @@ ${getMessage(locale, "cli_start")}`);
63
63
  filePath = path.join(outputDir, fileName);
64
64
  }
65
65
  fs.writeFileSync(filePath, markdown);
66
+ const parsedPath = path.parse(filePath);
67
+ const htmlFilePath = path.join(parsedPath.dir, `${parsedPath.name}.html`);
68
+ fs.writeFileSync(htmlFilePath, html);
66
69
  console.info(`
67
70
  ${getMessage(locale, "success_save")}`);
68
71
  console.info(getMessage(locale, "success_path", { path: filePath }));
72
+ console.info(getMessage(locale, "success_path", { path: htmlFilePath }));
69
73
  } catch (error) {
70
74
  console.error(`
71
75
  ${getMessage(locale, "error_app")}`, error);
@@ -5,6 +5,9 @@ import { HtmlToMarkdownOptions } from './types.js';
5
5
  *
6
6
  * @param url The URL of the web page to convert. / 変換対象のWebページのURL。
7
7
  * @param options Options for the conversion process. / 変換プロセスのオプション。
8
- * @returns A promise that resolves to the Markdown string. / Markdown文字列を解決するPromise。
8
+ * @returns A promise that resolves to an object containing the HTML and Markdown strings. / HTMLとMarkdown文字列を含むオブジェクトを解決するPromise。
9
9
  */
10
- export declare const htmlToMarkdown: (url: string, options?: HtmlToMarkdownOptions) => Promise<string>;
10
+ export declare const htmlToMarkdown: (url: string, options?: HtmlToMarkdownOptions) => Promise<{
11
+ html: string;
12
+ markdown: string;
13
+ }>;
@@ -3,5 +3,7 @@
3
3
  * Rehype プラグインのインデックス
4
4
  */
5
5
  export { rehypeAbsoluteLinks } from './rehype-absolute-links.js';
6
+ export { rehypeNamedAnchors } from './rehype-named-anchors.js';
7
+ export { rehypeParagraphWrapper } from './rehype-paragraph-wrapper.js';
6
8
  export { rehypeSanitizeHtml } from './rehype-sanitize-html.js';
7
9
  export { rehypeWikipediaFootnotes } from './rehype-wikipedia-footnotes.js';
@@ -0,0 +1,2 @@
1
+ import { Root } from 'hast';
2
+ export declare const rehypeNamedAnchors: () => (tree: Root) => void;
@@ -0,0 +1,6 @@
1
+ import { Root } from 'hast';
2
+ /**
3
+ * A rehype plugin to wrap an element and its adjacent anchor span with a paragraph.
4
+ * 要素とそれに隣接するアンカースパンを段落でラップする rehype プラグイン。
5
+ */
6
+ export declare const rehypeParagraphWrapper: () => (tree: Root) => void;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/html-to-markdown",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "HTML to Markdown converter",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,12 +20,15 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "scripts": {
23
+ "dev": "NODE_OPTIONS='--enable-source-maps' vite-node --inspect=9229 --watch main.ts --",
24
+ "debug": "NODE_OPTIONS='--enable-source-maps' vite-node --inspect-brk=9229 main.ts --",
23
25
  "start": "vite-node main.ts",
24
26
  "build": "vite build",
25
27
  "lint": "biome lint app/",
26
28
  "format": "biome format --write",
27
29
  "check": "biome check --fix src/",
28
30
  "test": "vitest run",
31
+ "test1": "pnpm -F @aiquants/html-to-markdown test --test-name-pattern 'rehypeNamedAnchors plugin'",
29
32
  "coverage": "vitest run --coverage"
30
33
  },
31
34
  "keywords": [