@aiquants/html-to-markdown 0.1.7 → 0.2.0

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/README.md CHANGED
@@ -133,9 +133,35 @@ npx --package=@aiquants/html-to-markdown aiq-html2md-mcp-stream
133
133
  #### MCP Tools Available
134
134
 
135
135
  - **html_to_markdown**: Convert HTML content from a URL to Markdown format
136
- - `url` (required): The URL of the web page to convert
136
+ - `url` (required*): The URL of the web page to convert
137
+ - `html_content` (required*): HTML content as a string to convert (alternative to URL)
137
138
  - `locale` (optional): Browser locale (`en-US` or `ja-JP`, defaults to `en-US`)
138
139
  - `output_format` (optional): Output format (`markdown`, `html`, or `both`, defaults to `markdown`)
140
+ - `save_path` (optional): Full path (including filename) where to save the converted content
141
+ - `save_directory` (optional): Directory path where to save the converted content with auto-generated filename
142
+
143
+ *Either `url` or `html_content` is required
144
+
145
+ **File Saving Options:**
146
+
147
+ - **`save_path`**: Specify the complete file path including filename and extension where you want to save the content.
148
+ - Example: `/path/to/output/my-page.md`
149
+ - The directory will be created automatically if it doesn't exist.
150
+ - You cannot use both `save_path` and `save_directory` at the same time.
151
+
152
+ - **`save_directory`**: Specify only the directory where you want to save the file. The filename will be auto-generated based on the URL.
153
+ - Example: `/path/to/output/` (filename will be auto-generated like `page.md`)
154
+ - For URLs like `https://example.com/articles/my-article`, the filename becomes `my-article.md`
155
+ - The directory will be created automatically if it doesn't exist.
156
+ - You cannot use both `save_path` and `save_directory` at the same time.
157
+
158
+ **File Saving Behavior:**
159
+
160
+ - When `output_format` is `markdown`: Only a `.md` file is saved
161
+ - When `output_format` is `html`: Only a `.html` file is saved
162
+ - When `output_format` is `both`: Both `.md` and `.html` files are saved
163
+ - With `save_path`: Files are saved as `path.md` and `path.html`
164
+ - With `save_directory`: Files are saved as `filename.md` and `filename.html`
139
165
 
140
166
  - **html_to_markdown_streamable**: Same as above but with streamable MCP support for progress updates
141
167
 
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 r=require("./core-D2dlEt1l.cjs"),t=require("./mcp-server-D4Mj1IQ9.cjs"),c=require("./mcp-server-streamable-Bu7QAr3W.cjs");exports.getMessage=r.getMessage,exports.htmlToMarkdown=r.htmlToMarkdown,exports.createMcpServer=t.createMcpServer,exports.runMcpServer=t.runMcpServer,exports.createStreamableMcpServer=c.createStreamableMcpServer,exports.runStreamableMcpServer=c.runStreamableMcpServer;
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 r=require("./core-D2dlEt1l.cjs"),t=require("./mcp-server-B5lrAMe3.cjs"),c=require("./mcp-server-streamable-B3R3qi85.cjs");exports.getMessage=r.getMessage,exports.htmlToMarkdown=r.htmlToMarkdown,exports.createMcpServer=t.createMcpServer,exports.runMcpServer=t.runMcpServer,exports.createStreamableMcpServer=c.createStreamableMcpServer,exports.runStreamableMcpServer=c.runStreamableMcpServer;
package/dist/index.js CHANGED
@@ -6,8 +6,8 @@ Object.assign(global, {
6
6
  self: window
7
7
  });
8
8
  import { g, h } from "./core-DU_246Q7.js";
9
- import { c, r } from "./mcp-server-CpmnW1p8.js";
10
- import { c as c2, r as r2 } from "./mcp-server-streamable-CQD5zK30.js";
9
+ import { c, r } from "./mcp-server-Ct2TYHcU.js";
10
+ import { c as c2, r as r2 } from "./mcp-server-streamable-C6-qOLdK.js";
11
11
  export {
12
12
  c as createMcpServer,
13
13
  c2 as createStreamableMcpServer,
@@ -0,0 +1 @@
1
+ const t=new(require("happy-dom").Window);Object.assign(global,{document:t.document,window:t,self:t});const e=require("./version-DTaUKNBS.cjs"),r=require("node:process"),n=require("fs"),o=require("path"),a=require("./core-D2dlEt1l.cjs");class s{append(t){this._buffer=this._buffer?Buffer.concat([this._buffer,t]):t}readMessage(){if(!this._buffer)return null;const t=this._buffer.indexOf("\n");if(-1===t)return null;const r=this._buffer.toString("utf8",0,t).replace(/\r$/,"");return this._buffer=this._buffer.subarray(t+1),function(t){return e.JSONRPCMessageSchema.parse(JSON.parse(t))}(r)}clear(){this._buffer=void 0}}class i{constructor(t=r.stdin,e=r.stdout){this._stdin=t,this._stdout=e,this._readBuffer=new s,this._started=!1,this._ondata=t=>{this._readBuffer.append(t),this.processReadBuffer()},this._onerror=t=>{var e;null===(e=this.onerror)||void 0===e||e.call(this,t)}}async start(){if(this._started)throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");this._started=!0,this._stdin.on("data",this._ondata),this._stdin.on("error",this._onerror)}processReadBuffer(){for(var t,e;;)try{const e=this._readBuffer.readMessage();if(null===e)break;null===(t=this.onmessage)||void 0===t||t.call(this,e)}catch(r){null===(e=this.onerror)||void 0===e||e.call(this,r)}}async close(){var t;this._stdin.off("data",this._ondata),this._stdin.off("error",this._onerror);0===this._stdin.listenerCount("data")&&this._stdin.pause(),this._readBuffer.clear(),null===(t=this.onclose)||void 0===t||t.call(this)}send(t){return new Promise(e=>{const r=function(t){return JSON.stringify(t)+"\n"}(t);this._stdout.write(r)?e():this._stdout.once("drain",e)})}}function c(t,e){if(t&&"direct-html-input"!==t)try{let e=new URL(t).pathname.split("/").pop()||"page";return e=e.replace(/\.[^.]*$/,""),e=e.replace(/[<>:"/\\|?*]/g,"_"),e&&!e.match(/^\.+$/)||(e="page"),e}catch{return"page"}return"page"}async function h(t,e,r,a,s){if(!e&&!r)return null;let i;if(e)i=o.resolve(e);else{if(!r)return null;{const t=c(a),e="html"===s?".html":".md";i=o.resolve(r,`${t}${e}`)}}return await n.promises.mkdir(o.dirname(i),{recursive:!0}),await n.promises.writeFile(i,t,"utf-8"),i}class l{constructor(){this.version=e.getPackageVersion(),this.server=new e.Server({name:"@aiquants/html-to-markdown",version:this.version,description:"A tool to dynamically fetch a web page from a given URL and convert it to Markdown."},{capabilities:{tools:{}}}),this.setupToolHandlers()}setupToolHandlers(){this.server.setRequestHandler(e.ListToolsRequestSchema,async()=>({tools:[{name:"html_to_markdown",description:"Convert HTML content from a URL or HTML string to Markdown format. Supports dynamic content loading and preserves table structures.",inputSchema:{type:"object",properties:{url:{type:"string",description:"The URL of the web page to convert to Markdown"},html_content:{type:"string",description:"HTML content as a string to convert to Markdown (alternative to URL)"},locale:{type:"string",description:"The locale to use for browser context (en-US or ja-JP)",enum:["en-US","ja-JP"],default:"en-US"},output_format:{type:"string",description:"Output format: 'markdown' (default), 'html', or 'both'",enum:["markdown","html","both"],default:"markdown"},save_path:{type:"string",description:"Full path (including filename) where to save the converted content. Cannot be used with save_directory."},save_directory:{type:"string",description:"Directory path where to save the converted content with auto-generated filename. Cannot be used with save_path."}},anyOf:[{required:["url"]},{required:["html_content"]}]}}]})),this.server.setRequestHandler(e.CallToolRequestSchema,async t=>{if("html_to_markdown"===t.params.name)return await this.handleHtmlToMarkdown(t.params.arguments);throw new Error(`Unknown tool: ${t.params.name}`)})}async handleHtmlToMarkdown(t){try{const e=function(t){if(!t||"object"!=typeof t)throw new Error("Arguments are required");const{url:e,html_content:r,locale:n="en-US",output_format:o="markdown",save_path:a,save_directory:s}=t;if(!e&&!r)throw new Error("Either 'url' or 'html_content' is required");if(e&&"string"!=typeof e)throw new Error("'url' must be a string");if(r&&"string"!=typeof r)throw new Error("'html_content' must be a string");if(n&&!["en-US","ja-JP"].includes(n))throw new Error("'locale' must be 'en-US' or 'ja-JP'");if(o&&!["markdown","html","both"].includes(o))throw new Error("'output_format' must be 'markdown', 'html', or 'both'");if(a&&"string"!=typeof a)throw new Error("'save_path' must be a string");if(s&&"string"!=typeof s)throw new Error("'save_directory' must be a string");if(a&&s)throw new Error("Cannot specify both 'save_path' and 'save_directory'");return{url:e,html_content:r,locale:n,output_format:o,save_path:a,save_directory:s}}(t),{url:r,html_content:n,locale:s="en-US",output_format:i="markdown",save_path:l,save_directory:d}=e;if(r)try{new URL(r)}catch{throw new Error(`Invalid URL: ${r}`)}const u=await a.htmlToMarkdown(r||"direct-html-input",{locale:s,htmlContent:n}),m=[];if(l||d)switch(i){case"markdown":{const t=await h(u.markdown,l,d,r,"markdown");t&&m.push(t);break}case"html":{const t=await h(u.html,l,d,r,"html");t&&m.push(t);break}case"both":{const t=l?l.endsWith(".md")?l:`${l}.md`:d?o.join(d,`${c(r)}.md`):void 0;if(t){const e=await h(u.markdown,t,void 0,r,"markdown");e&&m.push(e)}const e=l?l.endsWith(".html")?l:`${l}.html`:d?o.join(d,`${c(r)}.html`):void 0;if(e){const t=await h(u.html,e,void 0,r,"html");t&&m.push(t)}break}}let f="";switch(i){case"markdown":f=u.markdown;break;case"html":f=u.html;break;case"both":f=`=== Markdown ===\n${u.markdown}\n\n=== HTML ===\n${u.html}`;break;default:throw new Error(`Invalid output format: ${i}`)}m.length>0&&(f+=`\n\n=== Files Saved ===\n${m.map(t=>`- ${t}`).join("\n")}`);return{content:[{type:"text",text:f}],isError:!1}}catch(e){return{content:[{type:"text",text:`Error: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}async start(){const t=new i;await this.server.connect(t)}}const d=()=>new l;exports.createMcpServer=d,exports.runMcpServer=async()=>{const t=d();await t.start()};
@@ -7,6 +7,8 @@ Object.assign(global, {
7
7
  });
8
8
  import { J as JSONRPCMessageSchema, g as getPackageVersion, S as Server, L as ListToolsRequestSchema, C as CallToolRequestSchema } from "./version-BRVBmpor.js";
9
9
  import process from "node:process";
10
+ import { promises } from "fs";
11
+ import { join, resolve, dirname } from "path";
10
12
  import { h as htmlToMarkdown } from "./core-DU_246Q7.js";
11
13
  class ReadBuffer {
12
14
  append(chunk) {
@@ -86,12 +88,12 @@ class StdioServerTransport {
86
88
  (_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
87
89
  }
88
90
  send(message) {
89
- return new Promise((resolve) => {
91
+ return new Promise((resolve2) => {
90
92
  const json = serializeMessage(message);
91
93
  if (this._stdout.write(json)) {
92
- resolve();
94
+ resolve2();
93
95
  } else {
94
- this._stdout.once("drain", resolve);
96
+ this._stdout.once("drain", resolve2);
95
97
  }
96
98
  });
97
99
  }
@@ -100,7 +102,7 @@ function parseHtmlToMarkdownArgs(args) {
100
102
  if (!args || typeof args !== "object") {
101
103
  throw new Error("Arguments are required");
102
104
  }
103
- const { url, html_content, locale = "en-US", output_format = "markdown" } = args;
105
+ const { url, html_content, locale = "en-US", output_format = "markdown", save_path, save_directory } = args;
104
106
  if (!url && !html_content) {
105
107
  throw new Error("Either 'url' or 'html_content' is required");
106
108
  }
@@ -116,13 +118,59 @@ function parseHtmlToMarkdownArgs(args) {
116
118
  if (output_format && !["markdown", "html", "both"].includes(output_format)) {
117
119
  throw new Error("'output_format' must be 'markdown', 'html', or 'both'");
118
120
  }
121
+ if (save_path && typeof save_path !== "string") {
122
+ throw new Error("'save_path' must be a string");
123
+ }
124
+ if (save_directory && typeof save_directory !== "string") {
125
+ throw new Error("'save_directory' must be a string");
126
+ }
127
+ if (save_path && save_directory) {
128
+ throw new Error("Cannot specify both 'save_path' and 'save_directory'");
129
+ }
119
130
  return {
120
131
  url,
121
132
  html_content,
122
133
  locale,
123
- output_format
134
+ output_format,
135
+ save_path,
136
+ save_directory
124
137
  };
125
138
  }
139
+ function generateFilename(url, outputFormat) {
140
+ if (url && url !== "direct-html-input") {
141
+ try {
142
+ const urlObj = new URL(url);
143
+ let filename = urlObj.pathname.split("/").pop() || "page";
144
+ filename = filename.replace(/\.[^.]*$/, "");
145
+ filename = filename.replace(/[<>:"/\\|?*]/g, "_");
146
+ if (!filename || filename.match(/^\.+$/)) {
147
+ filename = "page";
148
+ }
149
+ return filename;
150
+ } catch {
151
+ return "page";
152
+ }
153
+ }
154
+ return "page";
155
+ }
156
+ async function saveContentToFile(content, savePath, saveDirectory, url, outputFormat) {
157
+ if (!savePath && !saveDirectory) {
158
+ return null;
159
+ }
160
+ let finalPath;
161
+ if (savePath) {
162
+ finalPath = resolve(savePath);
163
+ } else if (saveDirectory) {
164
+ const filename = generateFilename(url);
165
+ const extension = outputFormat === "html" ? ".html" : ".md";
166
+ finalPath = resolve(saveDirectory, `${filename}${extension}`);
167
+ } else {
168
+ return null;
169
+ }
170
+ await promises.mkdir(dirname(finalPath), { recursive: true });
171
+ await promises.writeFile(finalPath, content, "utf-8");
172
+ return finalPath;
173
+ }
126
174
  class HtmlToMarkdownMcpServer {
127
175
  constructor() {
128
176
  this.version = getPackageVersion();
@@ -173,6 +221,14 @@ class HtmlToMarkdownMcpServer {
173
221
  description: "Output format: 'markdown' (default), 'html', or 'both'",
174
222
  enum: ["markdown", "html", "both"],
175
223
  default: "markdown"
224
+ },
225
+ save_path: {
226
+ type: "string",
227
+ description: "Full path (including filename) where to save the converted content. Cannot be used with save_directory."
228
+ },
229
+ save_directory: {
230
+ type: "string",
231
+ description: "Directory path where to save the converted content with auto-generated filename. Cannot be used with save_path."
176
232
  }
177
233
  },
178
234
  anyOf: [{ required: ["url"] }, { required: ["html_content"] }]
@@ -198,7 +254,7 @@ class HtmlToMarkdownMcpServer {
198
254
  async handleHtmlToMarkdown(args) {
199
255
  try {
200
256
  const validatedArgs = parseHtmlToMarkdownArgs(args);
201
- const { url, html_content, locale = "en-US", output_format = "markdown" } = validatedArgs;
257
+ const { url, html_content, locale = "en-US", output_format = "markdown", save_path, save_directory } = validatedArgs;
202
258
  if (url) {
203
259
  try {
204
260
  new URL(url);
@@ -210,39 +266,64 @@ class HtmlToMarkdownMcpServer {
210
266
  locale,
211
267
  htmlContent: html_content
212
268
  });
213
- let content;
269
+ const savedPaths = [];
270
+ if (save_path || save_directory) {
271
+ switch (output_format) {
272
+ case "markdown": {
273
+ const savedPath = await saveContentToFile(result.markdown, save_path, save_directory, url, "markdown");
274
+ if (savedPath) savedPaths.push(savedPath);
275
+ break;
276
+ }
277
+ case "html": {
278
+ const savedPath = await saveContentToFile(result.html, save_path, save_directory, url, "html");
279
+ if (savedPath) savedPaths.push(savedPath);
280
+ break;
281
+ }
282
+ case "both": {
283
+ const markdownPath = save_path ? save_path.endsWith(".md") ? save_path : `${save_path}.md` : save_directory ? join(save_directory, `${generateFilename(url)}.md`) : void 0;
284
+ if (markdownPath) {
285
+ const savedMarkdownPath = await saveContentToFile(result.markdown, markdownPath, void 0, url, "markdown");
286
+ if (savedMarkdownPath) savedPaths.push(savedMarkdownPath);
287
+ }
288
+ const htmlPath = save_path ? save_path.endsWith(".html") ? save_path : `${save_path}.html` : save_directory ? join(save_directory, `${generateFilename(url)}.html`) : void 0;
289
+ if (htmlPath) {
290
+ const savedHtmlPath = await saveContentToFile(result.html, htmlPath, void 0, url, "html");
291
+ if (savedHtmlPath) savedPaths.push(savedHtmlPath);
292
+ }
293
+ break;
294
+ }
295
+ }
296
+ }
297
+ let responseText = "";
214
298
  switch (output_format) {
215
299
  case "markdown":
216
- content = [
217
- {
218
- type: "text",
219
- text: result.markdown
220
- }
221
- ];
300
+ responseText = result.markdown;
222
301
  break;
223
302
  case "html":
224
- content = [
225
- {
226
- type: "text",
227
- text: result.html
228
- }
229
- ];
303
+ responseText = result.html;
230
304
  break;
231
305
  case "both":
232
- content = [
233
- {
234
- type: "text",
235
- text: result.markdown
236
- },
237
- {
238
- type: "text",
239
- text: result.html
240
- }
241
- ];
306
+ responseText = `=== Markdown ===
307
+ ${result.markdown}
308
+
309
+ === HTML ===
310
+ ${result.html}`;
242
311
  break;
243
312
  default:
244
313
  throw new Error(`Invalid output format: ${output_format}`);
245
314
  }
315
+ if (savedPaths.length > 0) {
316
+ responseText += `
317
+
318
+ === Files Saved ===
319
+ ${savedPaths.map((path) => `- ${path}`).join("\n")}`;
320
+ }
321
+ const content = [
322
+ {
323
+ type: "text",
324
+ text: responseText
325
+ }
326
+ ];
246
327
  return {
247
328
  content,
248
329
  isError: false