From 9bb6adcf5b397e4ff986eef984bfc48657a7440f Mon Sep 17 00:00:00 2001 From: Aran Roig Date: Fri, 17 Jul 2026 23:40:22 +0200 Subject: [PATCH] jdksjkj --- template/content/index.md | 25 +-- template/convert.js | 376 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 377 insertions(+), 24 deletions(-) diff --git a/template/content/index.md b/template/content/index.md index 0c686d0..a844b7f 100644 --- a/template/content/index.md +++ b/template/content/index.md @@ -1,24 +1 @@ -# Welcome to Nuxt Content Starter - -This is the main page displaying Markdown located at [content/index.md](https://github.com/nuxt/starter/blob/content/content/index.md). - -Move to [about](/about) page. - -## Manage your Contents - -Create new pages or modify the existing ones in `content/` directory. - -## Query & Render Pages - -You can find an example of querying contents and rendering them in a [catch-all page](https://github.com/nuxt/starter/blob/content/app/pages/%5B...slug%5D.vue) - -## Integrate Vue Component - -::alert{color="green"} -The current [alert](https://github.com/nuxt/starter/blob/content/app/components/Alert.vue) and the [counter](https://github.com/nuxt/starter/blob/content/app/components/Counter.vue) below are `Vue` components integrated into the Markdown. -:: - -::counter -:: - -Checkout out the [documentation](https://content.nuxt.com/docs/getting-started) to learn more. +Hola que tal! \ No newline at end of file diff --git a/template/convert.js b/template/convert.js index e69de29..e2c8ebb 100644 --- a/template/convert.js +++ b/template/convert.js @@ -0,0 +1,376 @@ +#!/usr/bin/env node + +/** + * obsidian-to-nuxt.js + * + * Converts an Obsidian vault's Markdown files to Nuxt Content-compatible pages. + * + * Transformations applied: + * 1. [[Wiki links]] → [Wiki links](/slug) + * 2. [[Wiki links|Alias]] → [Alias](/slug) + * 3. ![[image.png]] → / NuxtImg tag + * 4. ![[note]] → MDC ::include component + * 5. Callouts > [!TYPE] Title → MDC ::callout{type} blocks + * 6. Block IDs ^block-id → anchors + * 7. Block refs [[note#^ref]] → [note](/note#block-ref) + * 8. Heading links [[note#Head]] → [note](/note#heading-slug) + * 9. Inline #tags → stripped (kept in frontmatter.tags) + * 10. Frontmatter → passed through; tags array merged + * + * Usage: + * node obsidian-to-nuxt.js --vault ./my-vault --out ./content --base-path /notes + * + * Options: + * --vault Path to Obsidian vault root (default: ./vault) + * --out Output directory for converted files (default: ./content) + * --base-path URL prefix for all generated links (default: "") + * --img-prefix URL prefix for embedded images (default: /images) + * --nuxt-img Use instead of (flag) + * --dry-run Print conversions without writing files (flag) + */ + +import fs from "fs"; +import path from "path"; +import { parseArgs } from "util"; + +// ─── CLI args ──────────────────────────────────────────────────────────────── + +const { values: args } = parseArgs({ + options: { + vault: { type: "string", default: "./vault" }, + out: { type: "string", default: "./content" }, + "base-path":{ type: "string", default: "" }, + "img-prefix":{ type: "string", default: "/images" }, + "nuxt-img": { type: "boolean", default: false }, + "dry-run": { type: "boolean", default: false }, + }, +}); + +const VAULT_DIR = path.resolve(args["vault"]); +const OUT_DIR = path.resolve(args["out"]); +const BASE_PATH = args["base-path"].replace(/\/$/, ""); +const IMG_PREFIX = args["img-prefix"].replace(/\/$/, ""); +const USE_NUXT_IMG = args["nuxt-img"]; +const DRY_RUN = args["dry-run"]; + +const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".avif"]); + +// ─── Utilities ─────────────────────────────────────────────────────────────── + +/** Turn a note name / file stem into a URL-safe slug */ +function slugify(name) { + return name + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^\w\-]/g, "") + .replace(/--+/g, "-"); +} + +/** Turn a heading string into a GitHub-style anchor slug */ +function headingSlug(heading) { + return heading + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^\w\-]/g, ""); +} + +/** Recursively collect all .md files in a directory */ +function collectMarkdownFiles(dir) { + const results = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + results.push(...collectMarkdownFiles(full)); + } else if (entry.isFile() && entry.name.endsWith(".md")) { + results.push(full); + } + } + return results; +} + +/** + * Build a lookup map: note stem (lowercase) → relative URL path + * Handles duplicate names by preferring the shortest path (vault-root wins). + */ +function buildNoteIndex(files) { + const index = new Map(); + for (const file of files) { + const stem = path.basename(file, ".md"); + const rel = path.relative(VAULT_DIR, file).replace(/\.md$/, ""); + const url = BASE_PATH + "/" + rel.split(path.sep).map(slugify).join("/"); + const key = stem.toLowerCase(); + if (!index.has(key) || rel.split(path.sep).length < index.get(key).depth) { + index.set(key, { url, depth: rel.split(path.sep).length }); + } + } + return new Map([...index.entries()].map(([k, v]) => [k, v.url])); +} + +// ─── Frontmatter helpers ───────────────────────────────────────────────────── + +const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/; + +function parseFrontmatter(content) { + const match = content.match(FRONTMATTER_RE); + if (!match) return { frontmatter: {}, body: content }; + + const raw = match[1]; + const body = content.slice(match[0].length); + + // Minimal YAML parser (handles string scalars and simple arrays) + const fm = {}; + let currentKey = null; + for (const line of raw.split("\n")) { + const listItem = line.match(/^\s+-\s+(.+)/); + if (listItem && currentKey) { + fm[currentKey] = Array.isArray(fm[currentKey]) ? fm[currentKey] : []; + fm[currentKey].push(listItem[1].trim()); + continue; + } + const kv = line.match(/^(\w[\w-]*):\s*(.*)/); + if (kv) { + currentKey = kv[1]; + const val = kv[2].trim(); + fm[currentKey] = val === "" ? [] : val.replace(/^["']|["']$/g, ""); + } + } + return { frontmatter: fm, body }; +} + +function serializeFrontmatter(fm) { + const lines = ["---"]; + for (const [key, val] of Object.entries(fm)) { + if (Array.isArray(val)) { + lines.push(`${key}:`); + for (const v of val) lines.push(` - ${v}`); + } else { + lines.push(`${key}: ${val}`); + } + } + lines.push("---"); + return lines.join("\n"); +} + +// ─── Inline-tag extractor ──────────────────────────────────────────────────── + +/** Pull #tags from body text (skip # inside code fences or headings) */ +function extractInlineTags(body) { + const tags = new Set(); + // Skip fenced code blocks + const stripped = body.replace(/```[\s\S]*?```/g, ""); + const tagRe = /(? { + return ` `; + }); +} + +// ─── Callouts ──────────────────────────────────────────────────────────────── + +const CALLOUT_START_RE = /^> \[!([A-Z]+)\]([+-]?)\s*(.*)/i; + +/** + * Convert Obsidian callout blocks to MDC (Nuxt Content) component syntax: + * + * > [!NOTE] Title ::callout{type="note" title="Title"} + * > content content + * :: + */ +function convertCallouts(body) { + const lines = body.split("\n"); + const out = []; + let inCallout = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (!inCallout) { + const m = line.match(CALLOUT_START_RE); + if (m) { + const type = m[1].toLowerCase(); + const title = m[3].trim(); + const titleAttr = title ? ` title="${title}"` : ""; + out.push(`::callout{type="${type}"${titleAttr}}`); + inCallout = true; + continue; + } + out.push(line); + } else { + // Continuation lines start with "> " or are blank (closing the block) + if (line.startsWith("> ")) { + out.push(line.slice(2)); // strip "> " + } else { + out.push("::"); // close MDC block + out.push(line); + inCallout = false; + } + } + } + + if (inCallout) out.push("::"); + return out.join("\n"); +} + +// ─── Embedded content ![[...]] ────────────────────────────────────────────── + +function convertEmbeds(body, noteIndex) { + return body.replace(/!\[\[([^\]]+?)\]\]/g, (_, ref) => { + const [rawName, size] = ref.split("|"); + const name = rawName.trim(); + const ext = path.extname(name).toLowerCase(); + + if (IMAGE_EXTS.has(ext)) { + // Image embed + const src = `${IMG_PREFIX}/${name}`; + const attrs = size ? ` width="${size.split("x")[0]}"` : ""; + if (USE_NUXT_IMG) { + return ``; + } + return `${path.basename(name, ext)}`; + } + + if (ext === ".mp4" || ext === ".webm") { + const src = `${IMG_PREFIX}/${name}`; + return ``; + } + + // Note embed → MDC include component + const stem = path.basename(name, ext || ".md"); + const slug = noteIndex.get(stem.toLowerCase()) ?? `${BASE_PATH}/${slugify(stem)}`; + return `::include{path="${slug}"}`; + }); +} + +// ─── Wiki links [[...]] ───────────────────────────────────────────────────── + +function convertWikiLinks(body, noteIndex) { + return body.replace(/\[\[([^\]]+?)\]\]/g, (_, ref) => { + // Split off alias: [[Note|Alias]] + const [target, alias] = ref.split("|").map(s => s.trim()); + + // Split off heading or block ref: [[Note#Heading]] [[Note#^block]] + const [notePart, anchor] = target.split("#").map(s => s.trim()); + + const stem = path.basename(notePart, ".md"); + const baseUrl = noteIndex.get(stem.toLowerCase()) ?? `${BASE_PATH}/${slugify(stem)}`; + + let url = baseUrl; + if (anchor) { + if (anchor.startsWith("^")) { + // Block reference + url += "#" + anchor.slice(1); + } else { + // Heading reference + url += "#" + headingSlug(anchor); + } + } + + const label = alias || (anchor ? `${stem} › ${anchor}` : stem); + return `[${label}](${url})`; + }); +} + +// ─── Main conversion ────────────────────────────────────────────────────────── + +function convertFile(content, noteIndex) { + let { frontmatter, body } = parseFrontmatter(content); + + // 1. Extract and merge inline tags into frontmatter + const inlineTags = extractInlineTags(body); + if (inlineTags.length) { + const existing = Array.isArray(frontmatter.tags) + ? frontmatter.tags + : frontmatter.tags + ? [frontmatter.tags] + : []; + frontmatter.tags = [...new Set([...existing, ...inlineTags])]; + body = stripInlineTags(body); + } + + // 2. Callout blocks (before wiki-link pass so "> [[link]]" still works) + body = convertCallouts(body); + + // 3. Block IDs + body = convertBlockIds(body); + + // 4. Embedded files ![[...]] + body = convertEmbeds(body, noteIndex); + + // 5. Wiki links [[...]] + body = convertWikiLinks(body, noteIndex); + + const fmString = Object.keys(frontmatter).length + ? serializeFrontmatter(frontmatter) + "\n\n" + : ""; + + return fmString + body.trim() + "\n"; +} + +// ─── Entry point ────────────────────────────────────────────────────────────── + +function main() { + if (!fs.existsSync(VAULT_DIR)) { + console.error(`Vault directory not found: ${VAULT_DIR}`); + process.exit(1); + } + + const files = collectMarkdownFiles(VAULT_DIR); + const noteIndex = buildNoteIndex(files); + + console.log(`Found ${files.length} markdown files in ${VAULT_DIR}`); + + let converted = 0, skipped = 0; + + for (const file of files) { + const rel = path.relative(VAULT_DIR, file); + const outPath = path.join( + OUT_DIR, + rel + .split(path.sep) + .map((seg, i, arr) => i === arr.length - 1 ? seg : slugify(seg)) + .join(path.sep) + ); + + const content = fs.readFileSync(file, "utf8"); + let converted_content; + + try { + converted_content = convertFile(content, noteIndex); + } catch (err) { + console.warn(` ⚠ Skipping ${rel}: ${err.message}`); + skipped++; + continue; + } + + if (DRY_RUN) { + console.log(`\n${"─".repeat(60)}\n[DRY-RUN] ${rel} → ${path.relative(process.cwd(), outPath)}\n`); + console.log(converted_content.slice(0, 500) + (converted_content.length > 500 ? "\n…" : "")); + } else { + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, converted_content, "utf8"); + console.log(` ✓ ${rel} → ${path.relative(process.cwd(), outPath)}`); + } + converted++; + } + + console.log(`\nDone. ${converted} file(s) converted, ${skipped} skipped.`); +} + +main(); \ No newline at end of file