From 60fa284a359c1588f62c72204fa18810610f29ff Mon Sep 17 00:00:00 2001 From: "Mike P. Sinn" Date: Thu, 18 Dec 2025 12:54:11 -0600 Subject: [PATCH 1/4] Windows compatibility --- src/DirectoryManager.js | 11 ++++++++--- src/Importer.js | 7 ++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/DirectoryManager.js b/src/DirectoryManager.js index 75bbd6a..f8d713a 100644 --- a/src/DirectoryManager.js +++ b/src/DirectoryManager.js @@ -1,10 +1,15 @@ import fs from "graceful-fs"; +import path from "node:path"; class DirectoryManager { static getDirectory(pathname) { - let dirs = pathname.split("/"); - dirs.pop(); - return dirs.join("/"); + let dir = path.dirname(pathname); + // Return empty string for root directory to maintain backward compatibility + // (original code returned "" for "/test.html", not "/") + if (dir === "/" || dir === "\\") { + return ""; + } + return dir; } constructor() { diff --git a/src/Importer.js b/src/Importer.js index 36cd439..6e2842e 100644 --- a/src/Importer.js +++ b/src/Importer.js @@ -426,15 +426,16 @@ class Importer { let pathname = path.join(".", ...subdirs, path.normalize(fallbackPath)); let extension = contentType === "markdown" ? ".md" : ".html"; - if(pathname.endsWith("/")) { + // Check for trailing path separator (cross-platform: / or \) + if(pathname.endsWith("/") || pathname.endsWith(path.sep)) { if(this.isAssetsColocated()) { - return `${pathname}index${extension}`; + return path.join(pathname, `index${extension}`); } return `${pathname.slice(0, -1)}${extension}`; } if(this.isAssetsColocated()) { - return `${pathname}/index${extension}`; + return path.join(pathname, `index${extension}`); } return `${pathname}${extension}`; } From e465d3217d6bac2959a94d44751db9069d0b03eb Mon Sep 17 00:00:00 2001 From: "Mike P. Sinn" Date: Thu, 18 Dec 2025 13:25:53 -0600 Subject: [PATCH 2/4] Download podcast mp3s --- src/HtmlTransformer.js | 8 ++++++++ src/Importer.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/HtmlTransformer.js b/src/HtmlTransformer.js index bb2f7ed..3ba3044 100644 --- a/src/HtmlTransformer.js +++ b/src/HtmlTransformer.js @@ -16,6 +16,14 @@ class HtmlTransformer { return this.#fetcher.fetchAsset(rawUrl, entry); } + // Download podcast MP3 files from tags + if(tagName === "a" && attr === "href") { + // Check if URL points to an audio file + if(rawUrl.match(/\.(mp3|m4a|ogg|wav|flac)(\?.*)?$/i)) { + return this.#fetcher.fetchAsset(rawUrl, entry); + } + } + return rawUrl; } }; diff --git a/src/Importer.js b/src/Importer.js index 6e2842e..846f2ad 100644 --- a/src/Importer.js +++ b/src/Importer.js @@ -302,6 +302,36 @@ class Importer { return content; } + async processMarkdownAudioLinks(content, entry) { + if(!content || !this.shouldDownloadAssets()) { + return content; + } + + // Match markdown links to audio files: [text](url.mp3) or [text](url.mp3 "title") + // Captures: [1] = link text, [2] = URL only, [3] = file extension, [4] = optional title with quotes + const audioLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s\)]+\.(mp3|m4a|ogg|wav|flac)(?:\?[^\s\)"]*)?)\s*("[^"]*")?\)/gi; + + const matches = [...content.matchAll(audioLinkPattern)]; + + for(const match of matches) { + const [fullMatch, linkText, audioUrl, , title] = match; + try { + const localUrl = await this.fetcher.fetchAsset(audioUrl, entry); + // Replace the URL in the markdown link with the local path, preserving title if present + const replacement = title + ? `[${linkText}](${localUrl} ${title})` + : `[${linkText}](${localUrl})`; + content = content.replace(fullMatch, replacement); + } catch(error) { + // If download fails, keep the original URL + if(this.isVerbose) { + console.error(`Failed to download audio: ${audioUrl}`, error.message); + } + } + } + + return content; + } // Is used to filter getEntries and in toFiles (which also checks conflicts) shouldSkipEntry(entry) { @@ -357,6 +387,9 @@ class Importer { entry.content = await this.getTransformedContent(entry, isWritingToMarkdown); + // Process markdown links to audio files (podcast MP3s, etc.) + entry.content = await this.processMarkdownAudioLinks(entry.content, entry); + if(isWritingToMarkdown && Importer.shouldConvertToMarkdown(entry)) { entry.contentType = "markdown"; } From 494f323ec46f8c3c3227a1e3ac19459284e51ed0 Mon Sep 17 00:00:00 2001 From: "Mike P. Sinn" Date: Thu, 18 Dec 2025 13:31:51 -0600 Subject: [PATCH 3/4] generate package.json if not present --- cli.js | 2 ++ src/Importer.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/cli.js b/cli.js index 7d7e932..167f03e 100755 --- a/cli.js +++ b/cli.js @@ -170,3 +170,5 @@ await importer.toFiles(entries); importer.logResults(); +importer.generateScaffolding(); + diff --git a/src/Importer.js b/src/Importer.js index 846f2ad..314e04a 100644 --- a/src/Importer.js +++ b/src/Importer.js @@ -583,6 +583,59 @@ ${entry.content}`; Logger.log(content.join(" ")); } + + generateScaffolding() { + if(this.dryRun) { + return; + } + + const packageJsonPath = path.join(".", "package.json"); + const eleventyConfigPath = path.join(".", ".eleventy.js"); + + // Generate package.json if it doesn't exist + if(!fs.existsSync(packageJsonPath)) { + const packageJson = { + name: path.basename(process.cwd()), + version: "1.0.0", + description: "Eleventy site", + scripts: { + start: "eleventy --serve", + build: "eleventy", + clean: "rm -rf _site" + }, + devDependencies: { + "@11ty/eleventy": "^3.0.0" + } + }; + + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", { encoding: "utf8" }); + if(this.isVerbose) { + Logger.log(kleur.green(`Created ${packageJsonPath}`)); + } + } + + // Generate .eleventy.js if it doesn't exist + if(!fs.existsSync(eleventyConfigPath)) { + const eleventyConfig = `module.exports = function(eleventyConfig) { + // Copy assets to output + eleventyConfig.addPassthroughCopy("${this.#outputFolder}/assets"); + eleventyConfig.addPassthroughCopy("${this.#outputFolder}/**/assets"); + + return { + dir: { + input: "${this.#outputFolder}", + output: "_site" + } + }; +}; +`; + + fs.writeFileSync(eleventyConfigPath, eleventyConfig, { encoding: "utf8" }); + if(this.isVerbose) { + Logger.log(kleur.green(`Created ${eleventyConfigPath}`)); + } + } + } } export { Importer }; From bb75cff7ea2fc37b00c434de297e400dede9c5eb Mon Sep 17 00:00:00 2001 From: "Mike P. Sinn" Date: Thu, 18 Dec 2025 13:54:22 -0600 Subject: [PATCH 4/4] decodeHtmlEntities in titles --- src/Importer.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Importer.js b/src/Importer.js index 314e04a..09c5b40 100644 --- a/src/Importer.js +++ b/src/Importer.js @@ -473,9 +473,20 @@ class Importer { return `${pathname}${extension}`; } + static decodeHtmlEntities(str) { + if (!str) return str; + const entities = { + '’': "'", '‘': "'", '“': '"', '”': '"', + '…': '...', '™': '™', '&': '&', '&': '&', + '–': '–', '—': '—', '"': '"', ''': "'", + '<': '<', '>': '>' + }; + return str.replace(/&#?[\w\d]+;/g, match => entities[match] || match); + } + static convertEntryToYaml(entry) { let data = {}; - data.title = entry.title; + data.title = Importer.decodeHtmlEntities(entry.title); data.authors = entry.authors; data.date = entry.date; data.metadata = entry.metadata || {};