@hutusi/amytis 1.5.5
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/.github/workflows/ci.yml +33 -0
- package/.github/workflows/publish.yml +53 -0
- package/AGENTS.md +41 -0
- package/CLAUDE.md +200 -0
- package/GEMINI.md +84 -0
- package/README.md +172 -0
- package/TODO.md +76 -0
- package/bun.lock +1530 -0
- package/content/about.mdx +23 -0
- package/content/books/sample-book/index.mdx +24 -0
- package/content/books/sample-book/introduction.mdx +34 -0
- package/content/books/sample-book/setup.mdx +48 -0
- package/content/books/sample-book/writing-content.mdx +49 -0
- package/content/flows/2026/02/05.md +8 -0
- package/content/flows/2026/02/10.mdx +8 -0
- package/content/flows/2026/02/15.md +8 -0
- package/content/flows/2026/02/18.mdx +14 -0
- package/content/posts/2026-01-12-the-art-of-algorithms.mdx +49 -0
- package/content/posts/2026-01-15-nested-image-test/images/test.svg +5 -0
- package/content/posts/2026-01-15-nested-image-test/index.mdx +27 -0
- package/content/posts/2026-01-21-kitchen-sink/assets/test.svg +5 -0
- package/content/posts/2026-01-21-kitchen-sink/index.mdx +169 -0
- package/content/posts/asynchronous-javascript.mdx +49 -0
- package/content/posts/draft-post.mdx +13 -0
- package/content/posts/future-post.mdx +12 -0
- package/content/posts/legacy-markdown.md +60 -0
- package/content/posts/markdown-features.mdx +78 -0
- package/content/posts/modern-css-layouts.mdx +45 -0
- package/content/posts/multilingual-test.mdx +124 -0
- package/content/posts/syntax-highlighting-showcase.mdx +528 -0
- package/content/posts/understanding-react-hooks.mdx +48 -0
- package/content/posts/welcome-to-amytis.mdx +21 -0
- package/content/posts//344/270/255/346/226/207/346/265/213/350/257/225/346/226/207/347/253/240.mdx +54 -0
- package/content/series/ai-nexus-weekly/index.mdx +10 -0
- package/content/series/ai-nexus-weekly/week-1.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-10.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-11.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-12.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-2.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-3.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-4.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-5.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-6.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-7.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-8.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-9.mdx +20 -0
- package/content/series/digital-garden/01-philosophy/index.mdx +23 -0
- package/content/series/digital-garden/01-philosophy.mdx +30 -0
- package/content/series/digital-garden/02-architecture.mdx +19 -0
- package/content/series/digital-garden/index.mdx +11 -0
- package/content/series/markdown-showcase/index.mdx +11 -0
- package/content/series/markdown-showcase/mathematical-notation.mdx +32 -0
- package/content/series/markdown-showcase/syntax-highlighting.mdx +119 -0
- package/content/series/markdown-showcase/visuals-and-diagrams.mdx +27 -0
- package/content/series/nextjs-deep-dive/01-getting-started.mdx +66 -0
- package/content/series/nextjs-deep-dive/02-routing-mastery/assets/diagram.svg +8 -0
- package/content/series/nextjs-deep-dive/02-routing-mastery/assets/m-p-model.png +0 -0
- package/content/series/nextjs-deep-dive/02-routing-mastery/index.mdx +138 -0
- package/content/series/nextjs-deep-dive/index.mdx +12 -0
- package/docs/ARCHITECTURE.md +103 -0
- package/docs/CONTRIBUTING.md +86 -0
- package/docs/deployment.md +319 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +25 -0
- package/package.json +81 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon.svg +9 -0
- package/public/logo.svg +11 -0
- package/public/next-image-export-optimizer-hashes.json +7 -0
- package/public/next.svg +1 -0
- package/public/screenshot.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/copy-assets.ts +211 -0
- package/scripts/new-flow.ts +47 -0
- package/scripts/new-from-images.ts +141 -0
- package/scripts/new-from-pdf.ts +105 -0
- package/scripts/new-post.ts +98 -0
- package/scripts/new-series.ts +40 -0
- package/scripts/series-draft.ts +136 -0
- package/site.config.ts +91 -0
- package/src/app/[slug]/page.tsx +67 -0
- package/src/app/archive/page.tsx +147 -0
- package/src/app/authors/[author]/page.tsx +210 -0
- package/src/app/books/[slug]/[chapter]/page.tsx +54 -0
- package/src/app/books/[slug]/page.tsx +156 -0
- package/src/app/books/page.tsx +63 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/feed.xml/route.ts +44 -0
- package/src/app/flows/[year]/[month]/[day]/page.tsx +105 -0
- package/src/app/flows/[year]/[month]/page.tsx +72 -0
- package/src/app/flows/[year]/page.tsx +82 -0
- package/src/app/flows/page/[page]/page.tsx +63 -0
- package/src/app/flows/page.tsx +38 -0
- package/src/app/globals.css +406 -0
- package/src/app/layout.tsx +114 -0
- package/src/app/page/[page]/page.tsx +60 -0
- package/src/app/page.tsx +110 -0
- package/src/app/posts/[slug]/page.tsx +119 -0
- package/src/app/posts/page/[page]/page.tsx +58 -0
- package/src/app/posts/page.tsx +40 -0
- package/src/app/search.json/route.ts +49 -0
- package/src/app/series/[slug]/page/[page]/page.tsx +141 -0
- package/src/app/series/[slug]/page.tsx +139 -0
- package/src/app/series/page.tsx +96 -0
- package/src/app/sitemap.ts +112 -0
- package/src/app/tags/[tag]/page.tsx +76 -0
- package/src/app/tags/page.tsx +37 -0
- package/src/components/Analytics.tsx +49 -0
- package/src/components/AuthorStats.tsx +34 -0
- package/src/components/BookMobileNav.tsx +171 -0
- package/src/components/BookSidebar.tsx +275 -0
- package/src/components/CodeBlock.tsx +110 -0
- package/src/components/Comments.tsx +63 -0
- package/src/components/CoverImage.tsx +93 -0
- package/src/components/CuratedSeriesSection.tsx +124 -0
- package/src/components/ExternalLinks.tsx +45 -0
- package/src/components/FeaturedStoriesSection.tsx +106 -0
- package/src/components/FlowCalendarSidebar.tsx +249 -0
- package/src/components/FlowContent.tsx +96 -0
- package/src/components/FlowTimelineEntry.tsx +34 -0
- package/src/components/Footer.tsx +104 -0
- package/src/components/Hero.tsx +126 -0
- package/src/components/HorizontalScroll.tsx +128 -0
- package/src/components/LanguageProvider.tsx +80 -0
- package/src/components/LanguageSwitch.tsx +17 -0
- package/src/components/LatestWritingSection.tsx +45 -0
- package/src/components/MarkdownRenderer.tsx +135 -0
- package/src/components/Mermaid.tsx +89 -0
- package/src/components/Navbar.tsx +243 -0
- package/src/components/PageHeader.tsx +39 -0
- package/src/components/Pagination.tsx +120 -0
- package/src/components/PostCard.tsx +30 -0
- package/src/components/PostList.tsx +104 -0
- package/src/components/PostSidebar.tsx +225 -0
- package/src/components/ReadingProgressBar.tsx +37 -0
- package/src/components/RecentNotesSection.tsx +56 -0
- package/src/components/RelatedPosts.tsx +34 -0
- package/src/components/Search.tsx +151 -0
- package/src/components/SelectedBooksSection.tsx +80 -0
- package/src/components/SeriesCatalog.tsx +112 -0
- package/src/components/SeriesList.tsx +167 -0
- package/src/components/SeriesSidebar.tsx +132 -0
- package/src/components/SimpleLayoutHeader.tsx +38 -0
- package/src/components/Skeleton.tsx +131 -0
- package/src/components/TableOfContents.tsx +158 -0
- package/src/components/Tag.tsx +47 -0
- package/src/components/TagPageHeader.tsx +38 -0
- package/src/components/ThemeProvider.tsx +12 -0
- package/src/components/ThemeToggle.tsx +68 -0
- package/src/components/TranslatedText.tsx +13 -0
- package/src/fonts/Inter-Bold.woff2 +0 -0
- package/src/fonts/Inter-Regular.woff2 +0 -0
- package/src/fonts/LibreBaskerville-Bold.ttf +0 -0
- package/src/fonts/LibreBaskerville-Italic.ttf +0 -0
- package/src/fonts/LibreBaskerville-Regular.ttf +0 -0
- package/src/i18n/translations.ts +135 -0
- package/src/layouts/BookLayout.tsx +109 -0
- package/src/layouts/PostLayout.tsx +118 -0
- package/src/layouts/SimpleLayout.tsx +31 -0
- package/src/lib/i18n.ts +35 -0
- package/src/lib/markdown.test.ts +127 -0
- package/src/lib/markdown.ts +1067 -0
- package/src/lib/rehype-image-metadata.ts +54 -0
- package/src/lib/shuffle.ts +11 -0
- package/templates/default.mdx +13 -0
- package/tests/e2e/navigation.test.ts +51 -0
- package/tests/e2e/series-routes.test.ts +63 -0
- package/tests/e2e/smoke.test.ts +19 -0
- package/tests/integration/markdown-features.test.ts +54 -0
- package/tests/integration/posts.test.ts +57 -0
- package/tests/integration/reading-time-headings.test.ts +79 -0
- package/tests/integration/series-draft.test.ts +46 -0
- package/tests/integration/series.test.ts +79 -0
- package/tests/tooling/new-from-images.test.ts +173 -0
- package/tests/tooling/new-post.test.ts +72 -0
- package/tsconfig.json +34 -0
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hutusi/amytis",
|
|
3
|
+
"version": "1.5.5",
|
|
4
|
+
"description": "A high-performance digital garden and blog engine with Next.js 16 and Tailwind CSS v4",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/hutusi/amytis.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/hutusi/amytis/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/hutusi/amytis#readme",
|
|
13
|
+
"private": false,
|
|
14
|
+
"packageManager": "bun@1.3.4",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "next dev",
|
|
17
|
+
"build": "bun scripts/copy-assets.ts && next build && next-image-export-optimizer",
|
|
18
|
+
"build:dev": "bun scripts/copy-assets.ts && next build",
|
|
19
|
+
"validate": "bun run lint && bun run test && bun run build:dev",
|
|
20
|
+
"clean": "rm -rf .next out public/posts public/books public/flows",
|
|
21
|
+
"new": "bun scripts/new-post.ts",
|
|
22
|
+
"new-weekly": "bun scripts/new-post.ts --series ai-nexus-weekly --md --folder --prefix weekly",
|
|
23
|
+
"new-series": "bun scripts/new-series.ts",
|
|
24
|
+
"new-from-pdf": "bun scripts/new-from-pdf.ts",
|
|
25
|
+
"new-from-images": "bun scripts/new-from-images.ts",
|
|
26
|
+
"new-flow": "bun scripts/new-flow.ts",
|
|
27
|
+
"series-draft": "bun scripts/series-draft.ts",
|
|
28
|
+
"start": "next start",
|
|
29
|
+
"lint": "eslint",
|
|
30
|
+
"test": "bun test",
|
|
31
|
+
"test:unit": "bun test src",
|
|
32
|
+
"test:int": "bun test tests/integration",
|
|
33
|
+
"test:e2e": "bun test tests/e2e"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@giscus/react": "^3.1.0",
|
|
37
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
38
|
+
"fuse.js": "^7.1.0",
|
|
39
|
+
"github-slugger": "^2.0.0",
|
|
40
|
+
"gray-matter": "^4.0.3",
|
|
41
|
+
"image-size": "^2.0.2",
|
|
42
|
+
"mermaid": "^11.12.3",
|
|
43
|
+
"next": "16.1.6",
|
|
44
|
+
"next-image-export-optimizer": "^1.20.1",
|
|
45
|
+
"next-themes": "^0.4.6",
|
|
46
|
+
"react": "19.2.4",
|
|
47
|
+
"react-dom": "19.2.4",
|
|
48
|
+
"react-markdown": "^10.1.0",
|
|
49
|
+
"react-syntax-highlighter": "^16.1.0",
|
|
50
|
+
"rehype-katex": "^7.0.1",
|
|
51
|
+
"rehype-raw": "^7.0.0",
|
|
52
|
+
"rehype-slug": "^6.0.0",
|
|
53
|
+
"remark-gfm": "^4.0.1",
|
|
54
|
+
"remark-math": "^6.0.0",
|
|
55
|
+
"unist-util-visit": "^5.1.0",
|
|
56
|
+
"zod": "^4.3.6"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@tailwindcss/postcss": "^4.1.18",
|
|
60
|
+
"@types/bun": "^1.3.9",
|
|
61
|
+
"@types/image-size": "^0.8.0",
|
|
62
|
+
"@types/node": "^24.10.13",
|
|
63
|
+
"@types/react": "^19.2.14",
|
|
64
|
+
"@types/react-dom": "^19.2.3",
|
|
65
|
+
"@types/react-syntax-highlighter": "^15.5.13",
|
|
66
|
+
"babel-plugin-react-compiler": "1.0.0",
|
|
67
|
+
"eslint": "^9.0.0",
|
|
68
|
+
"eslint-config-next": "16.1.6",
|
|
69
|
+
"pdf-to-img": "^5.0.0",
|
|
70
|
+
"tailwindcss": "^4.1.18",
|
|
71
|
+
"typescript": "^5.9.3"
|
|
72
|
+
},
|
|
73
|
+
"ignoreScripts": [
|
|
74
|
+
"sharp",
|
|
75
|
+
"unrs-resolver"
|
|
76
|
+
],
|
|
77
|
+
"trustedDependencies": [
|
|
78
|
+
"sharp",
|
|
79
|
+
"unrs-resolver"
|
|
80
|
+
]
|
|
81
|
+
}
|
package/public/file.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
package/public/globe.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
package/public/icon.svg
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="#059669" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<!-- Letter A structure -->
|
|
3
|
+
<path d="M16 4 L7 28" />
|
|
4
|
+
<path d="M16 4 L25 28" />
|
|
5
|
+
<!-- Crossbar / Leaf stem -->
|
|
6
|
+
<path d="M11.5 18 H 20.5" />
|
|
7
|
+
<!-- Leaf accent -->
|
|
8
|
+
<path d="M20.5 18 Q 26 14 26 8 Q 23 12 20.5 18" fill="#059669" stroke="none" />
|
|
9
|
+
</svg>
|
package/public/logo.svg
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 32" fill="none">
|
|
2
|
+
<!-- Icon Part -->
|
|
3
|
+
<g>
|
|
4
|
+
<path d="M16 4 L7 28" stroke="#059669" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
<path d="M16 4 L25 28" stroke="#059669" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
6
|
+
<path d="M11.5 18 H 20.5" stroke="#059669" stroke-width="2" stroke-linecap="round" />
|
|
7
|
+
<path d="M20.5 18 Q 26 14 26 8 Q 23 12 20.5 18" fill="#059669" />
|
|
8
|
+
</g>
|
|
9
|
+
<!-- Text Part -->
|
|
10
|
+
<text x="36" y="24" font-family="serif" font-size="22" font-weight="bold" fill="#1c1917" letter-spacing="0.5">Amytis</text>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"posts/02-routing-mastery/assets/m-p-model.png": "fDmvlEkZnE-UCvPK4gmDkJD7SU8coOTl4iw5hpsmcWI=",
|
|
3
|
+
"posts/advanced-markdown/images/m-p-model.png": "BdYG3Wq+FcOnoSrZeJeb6x2V-LsB-gwvms3WQJFq+Wg=",
|
|
4
|
+
"posts/part-1/images/m-p-model.png": "94OaqjvkCmDcz+Iuhw14vHXYUzYl3Y8+wwoU1CQZ6l0=",
|
|
5
|
+
"posts/part-2-rich-content/images/m-p-model.png": "Vp5bOs9N2OHHjMM2PFntkkCl1uY7DleLymwrSHbecOA=",
|
|
6
|
+
"/screenshot.png": "dGCNrjp1oMIL3NrVh1VDW0K+7Pp-cI5KH6tW4-6o9zg="
|
|
7
|
+
}
|
package/public/next.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { siteConfig } from '../site.config';
|
|
4
|
+
|
|
5
|
+
const srcDir = path.join(process.cwd(), 'content', 'posts');
|
|
6
|
+
const seriesSrcDir = path.join(process.cwd(), 'content', 'series');
|
|
7
|
+
const booksSrcDir = path.join(process.cwd(), 'content', 'books');
|
|
8
|
+
const flowsSrcDir = path.join(process.cwd(), 'content', 'flows');
|
|
9
|
+
const destDir = path.join(process.cwd(), 'public', 'posts');
|
|
10
|
+
const booksDestDir = path.join(process.cwd(), 'public', 'books');
|
|
11
|
+
const flowsDestDir = path.join(process.cwd(), 'public', 'flows');
|
|
12
|
+
|
|
13
|
+
function copyRecursive(src: string, dest: string) {
|
|
14
|
+
if (!fs.existsSync(src)) return;
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(dest)) {
|
|
17
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
21
|
+
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const srcPath = path.join(src, entry.name);
|
|
24
|
+
const destPath = path.join(dest, entry.name);
|
|
25
|
+
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
copyRecursive(srcPath, destPath);
|
|
28
|
+
} else {
|
|
29
|
+
// Copy all files except markdown source
|
|
30
|
+
if (!entry.name.endsWith('.md') && !entry.name.endsWith('.mdx')) {
|
|
31
|
+
let shouldCopy = true;
|
|
32
|
+
if (fs.existsSync(destPath)) {
|
|
33
|
+
const srcStat = fs.statSync(srcPath);
|
|
34
|
+
const destStat = fs.statSync(destPath);
|
|
35
|
+
if (srcStat.mtimeMs <= destStat.mtimeMs) {
|
|
36
|
+
shouldCopy = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (shouldCopy) {
|
|
41
|
+
fs.copyFileSync(srcPath, destPath);
|
|
42
|
+
// console.log(`Copied: ${entry.name} -> ${destPath}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getSlugFromFilename(filename: string): string {
|
|
50
|
+
const nameWithoutExt = filename.replace(/\.mdx?$/, '');
|
|
51
|
+
const dateRegex = /^(\d{4}-\d{2}-\d{2})-(.*)$/;
|
|
52
|
+
const match = nameWithoutExt.match(dateRegex);
|
|
53
|
+
|
|
54
|
+
if (match && !siteConfig.includeDateInUrl) {
|
|
55
|
+
return match[2];
|
|
56
|
+
}
|
|
57
|
+
return nameWithoutExt;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function processPosts() {
|
|
61
|
+
if (fs.existsSync(srcDir)) {
|
|
62
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
63
|
+
|
|
64
|
+
entries.forEach((entry) => {
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
const targetName = getSlugFromFilename(entry.name);
|
|
67
|
+
const srcPostDir = path.join(srcDir, entry.name);
|
|
68
|
+
const destPostDir = path.join(destDir, targetName);
|
|
69
|
+
|
|
70
|
+
console.log(`Processing Post: ${entry.name} -> ${targetName}`);
|
|
71
|
+
copyRecursive(srcPostDir, destPostDir);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check if a directory is a post folder (contains index.md or index.mdx)
|
|
78
|
+
function isPostFolder(dirPath: string): boolean {
|
|
79
|
+
return fs.existsSync(path.join(dirPath, 'index.md')) ||
|
|
80
|
+
fs.existsSync(path.join(dirPath, 'index.mdx'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if a directory is an asset folder (not a post folder)
|
|
84
|
+
function isAssetFolder(dirPath: string): boolean {
|
|
85
|
+
return !isPostFolder(dirPath);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function processSeries() {
|
|
89
|
+
if (!fs.existsSync(seriesSrcDir)) return;
|
|
90
|
+
|
|
91
|
+
const seriesEntries = fs.readdirSync(seriesSrcDir, { withFileTypes: true });
|
|
92
|
+
|
|
93
|
+
seriesEntries.forEach((seriesEntry) => {
|
|
94
|
+
if (seriesEntry.isDirectory()) {
|
|
95
|
+
const seriesPath = path.join(seriesSrcDir, seriesEntry.name);
|
|
96
|
+
const items = fs.readdirSync(seriesPath, { withFileTypes: true });
|
|
97
|
+
|
|
98
|
+
// First pass: identify shared asset folders at series level (folders that are NOT posts)
|
|
99
|
+
const sharedAssetFolders = items
|
|
100
|
+
.filter(item => item.isDirectory() && isAssetFolder(path.join(seriesPath, item.name)))
|
|
101
|
+
.map(item => item.name);
|
|
102
|
+
|
|
103
|
+
// Process items in series folder
|
|
104
|
+
items.forEach(item => {
|
|
105
|
+
if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.mdx'))) {
|
|
106
|
+
// File-based post or series index
|
|
107
|
+
const targetSlug = item.name.startsWith('index.') ? seriesEntry.name : getSlugFromFilename(item.name);
|
|
108
|
+
const destPostDir = path.join(destDir, targetSlug);
|
|
109
|
+
|
|
110
|
+
console.log(`Processing Series File: ${item.name} -> ${targetSlug}`);
|
|
111
|
+
|
|
112
|
+
// Only copy shared asset folders (like images/, assets/), not sibling post folders
|
|
113
|
+
sharedAssetFolders.forEach(folderName => {
|
|
114
|
+
const srcPath = path.join(seriesPath, folderName);
|
|
115
|
+
const destPath = path.join(destPostDir, folderName);
|
|
116
|
+
copyRecursive(srcPath, destPath);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Copy any non-markdown files at the series root level
|
|
120
|
+
items.filter(sub => sub.isFile() && !sub.name.endsWith('.md') && !sub.name.endsWith('.mdx'))
|
|
121
|
+
.forEach(sub => {
|
|
122
|
+
const srcPath = path.join(seriesPath, sub.name);
|
|
123
|
+
const destPath = path.join(destPostDir, sub.name);
|
|
124
|
+
if (!fs.existsSync(destPostDir)) {
|
|
125
|
+
fs.mkdirSync(destPostDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
fs.copyFileSync(srcPath, destPath);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
} else if (item.isDirectory() && isPostFolder(path.join(seriesPath, item.name))) {
|
|
131
|
+
// Folder-based post: copy only its own assets
|
|
132
|
+
const targetSlug = getSlugFromFilename(item.name);
|
|
133
|
+
const itemSrcPath = path.join(seriesPath, item.name);
|
|
134
|
+
const destPostDir = path.join(destDir, targetSlug);
|
|
135
|
+
|
|
136
|
+
console.log(`Processing Series Post Folder: ${item.name} -> ${targetSlug}`);
|
|
137
|
+
|
|
138
|
+
// Copy everything from the post folder EXCEPT markdown files
|
|
139
|
+
const subItems = fs.readdirSync(itemSrcPath, { withFileTypes: true });
|
|
140
|
+
subItems.forEach(sub => {
|
|
141
|
+
const srcPath = path.join(itemSrcPath, sub.name);
|
|
142
|
+
const destPath = path.join(destPostDir, sub.name);
|
|
143
|
+
|
|
144
|
+
if (sub.isDirectory()) {
|
|
145
|
+
copyRecursive(srcPath, destPath);
|
|
146
|
+
} else if (!sub.name.endsWith('.md') && !sub.name.endsWith('.mdx')) {
|
|
147
|
+
if (!fs.existsSync(destPostDir)) {
|
|
148
|
+
fs.mkdirSync(destPostDir, { recursive: true });
|
|
149
|
+
}
|
|
150
|
+
fs.copyFileSync(srcPath, destPath);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function processBooks() {
|
|
160
|
+
if (!fs.existsSync(booksSrcDir)) return;
|
|
161
|
+
|
|
162
|
+
const entries = fs.readdirSync(booksSrcDir, { withFileTypes: true });
|
|
163
|
+
|
|
164
|
+
entries.forEach((entry) => {
|
|
165
|
+
if (entry.isDirectory()) {
|
|
166
|
+
const srcBookDir = path.join(booksSrcDir, entry.name);
|
|
167
|
+
const destBookDir = path.join(booksDestDir, entry.name);
|
|
168
|
+
|
|
169
|
+
console.log(`Processing Book: ${entry.name}`);
|
|
170
|
+
copyRecursive(srcBookDir, destBookDir);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function processFlows() {
|
|
176
|
+
if (!fs.existsSync(flowsSrcDir)) return;
|
|
177
|
+
|
|
178
|
+
// Walk content/flows/YYYY/MM/ structure for folder-based flows with co-located assets
|
|
179
|
+
const yearDirs = fs.readdirSync(flowsSrcDir, { withFileTypes: true });
|
|
180
|
+
for (const yearEntry of yearDirs) {
|
|
181
|
+
if (!yearEntry.isDirectory() || !/^\d{4}$/.test(yearEntry.name)) continue;
|
|
182
|
+
const yearPath = path.join(flowsSrcDir, yearEntry.name);
|
|
183
|
+
|
|
184
|
+
const monthDirs = fs.readdirSync(yearPath, { withFileTypes: true });
|
|
185
|
+
for (const monthEntry of monthDirs) {
|
|
186
|
+
if (!monthEntry.isDirectory() || !/^\d{2}$/.test(monthEntry.name)) continue;
|
|
187
|
+
const monthPath = path.join(yearPath, monthEntry.name);
|
|
188
|
+
|
|
189
|
+
const dayItems = fs.readdirSync(monthPath, { withFileTypes: true });
|
|
190
|
+
for (const dayItem of dayItems) {
|
|
191
|
+
// Only process folder-based flows (DD/ directories with index.mdx)
|
|
192
|
+
if (!dayItem.isDirectory()) continue;
|
|
193
|
+
const rawName = dayItem.name;
|
|
194
|
+
if (!/^\d{2}$/.test(rawName)) continue;
|
|
195
|
+
|
|
196
|
+
const srcFlowDir = path.join(monthPath, rawName);
|
|
197
|
+
const destFlowDir = path.join(flowsDestDir, yearEntry.name, monthEntry.name, rawName);
|
|
198
|
+
|
|
199
|
+
console.log(`Processing Flow: ${yearEntry.name}/${monthEntry.name}/${rawName}`);
|
|
200
|
+
copyRecursive(srcFlowDir, destFlowDir);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log('Copying assets...');
|
|
207
|
+
processPosts();
|
|
208
|
+
processSeries();
|
|
209
|
+
processBooks();
|
|
210
|
+
processFlows();
|
|
211
|
+
console.log('Assets copied successfully.');
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
const title = args.filter(arg => !arg.startsWith('--'))[0];
|
|
6
|
+
const useMdx = args.includes('--mdx');
|
|
7
|
+
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const year = String(now.getFullYear());
|
|
10
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
11
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
12
|
+
const dateStr = `${year}-${month}-${day}`;
|
|
13
|
+
|
|
14
|
+
const ext = useMdx ? '.mdx' : '.md';
|
|
15
|
+
const dirPath = path.join(process.cwd(), 'content', 'flows', year, month);
|
|
16
|
+
const targetPath = path.join(dirPath, `${day}${ext}`);
|
|
17
|
+
|
|
18
|
+
// Check if today's flow already exists (either .md or .mdx)
|
|
19
|
+
const altExt = useMdx ? '.md' : '.mdx';
|
|
20
|
+
const altPath = path.join(dirPath, `${day}${altExt}`);
|
|
21
|
+
|
|
22
|
+
if (fs.existsSync(targetPath)) {
|
|
23
|
+
console.error(`Error: Flow already exists at ${targetPath}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (fs.existsSync(altPath)) {
|
|
28
|
+
console.error(`Error: Flow already exists at ${altPath}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create parent directories if needed
|
|
33
|
+
if (!fs.existsSync(dirPath)) {
|
|
34
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const flowTitle = title || dateStr;
|
|
38
|
+
|
|
39
|
+
const content = `---
|
|
40
|
+
title: "${flowTitle}"
|
|
41
|
+
tags: []
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
fs.writeFileSync(targetPath, content);
|
|
47
|
+
console.log(`Created new flow: ${targetPath}`);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
|
|
6
|
+
// Parse arguments
|
|
7
|
+
const folderPath = args.find(arg => !arg.startsWith('--'));
|
|
8
|
+
const titleArgIndex = args.indexOf('--title');
|
|
9
|
+
const title = titleArgIndex > -1 ? args[titleArgIndex + 1] : '';
|
|
10
|
+
const sortArg = args.indexOf('--sort');
|
|
11
|
+
const sortOrder = sortArg > -1 ? args[sortArg + 1] : 'name'; // name, date, or none
|
|
12
|
+
const copyImages = !args.includes('--no-copy');
|
|
13
|
+
|
|
14
|
+
// Supported image extensions
|
|
15
|
+
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif'];
|
|
16
|
+
|
|
17
|
+
function isImageFile(filename: string): boolean {
|
|
18
|
+
const ext = path.extname(filename).toLowerCase();
|
|
19
|
+
return IMAGE_EXTENSIONS.includes(ext);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getImageFiles(dir: string, sort: string): string[] {
|
|
23
|
+
const files = fs.readdirSync(dir)
|
|
24
|
+
.filter(f => {
|
|
25
|
+
const filePath = path.join(dir, f);
|
|
26
|
+
return fs.statSync(filePath).isFile() && isImageFile(f);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
switch (sort) {
|
|
30
|
+
case 'name':
|
|
31
|
+
return files.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
32
|
+
case 'date':
|
|
33
|
+
return files.sort((a, b) => {
|
|
34
|
+
const statA = fs.statSync(path.join(dir, a));
|
|
35
|
+
const statB = fs.statSync(path.join(dir, b));
|
|
36
|
+
return statA.mtimeMs - statB.mtimeMs;
|
|
37
|
+
});
|
|
38
|
+
case 'none':
|
|
39
|
+
default:
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!folderPath) {
|
|
45
|
+
console.error('Please provide an image folder path.');
|
|
46
|
+
console.error('Usage: bun run new-from-images <folder> [--title "Post Title"] [--sort name|date|none] [--no-copy]');
|
|
47
|
+
console.error('');
|
|
48
|
+
console.error('Options:');
|
|
49
|
+
console.error(' --title Custom title for the post (default: folder name)');
|
|
50
|
+
console.error(' --sort Sort images by: name (default), date, or none');
|
|
51
|
+
console.error(' --no-copy Reference images in place instead of copying');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {
|
|
56
|
+
console.error(`Error: "${folderPath}" is not a valid directory.`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Get image files
|
|
61
|
+
const imageFiles = getImageFiles(folderPath, sortOrder);
|
|
62
|
+
|
|
63
|
+
if (imageFiles.length === 0) {
|
|
64
|
+
console.error(`Error: No image files found in "${folderPath}".`);
|
|
65
|
+
console.error(`Supported formats: ${IMAGE_EXTENSIONS.join(', ')}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`Found ${imageFiles.length} image(s) in folder.`);
|
|
70
|
+
|
|
71
|
+
// Generate slug from title or folder name
|
|
72
|
+
const folderName = path.basename(folderPath);
|
|
73
|
+
const postTitle = title || folderName.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
74
|
+
const slug = (title || folderName)
|
|
75
|
+
.toLowerCase()
|
|
76
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
77
|
+
.replace(/(^-|-$)+/g, '');
|
|
78
|
+
|
|
79
|
+
const date = new Date().toISOString().split('T')[0];
|
|
80
|
+
const dirName = `${date}-${slug}`;
|
|
81
|
+
const dirPath = path.join(process.cwd(), 'content', 'posts', dirName);
|
|
82
|
+
const imagesDir = path.join(dirPath, 'images');
|
|
83
|
+
|
|
84
|
+
// Check if post already exists
|
|
85
|
+
if (fs.existsSync(dirPath)) {
|
|
86
|
+
console.error(`Error: Post already exists at ${dirPath}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Create directories
|
|
91
|
+
fs.mkdirSync(imagesDir, { recursive: true });
|
|
92
|
+
console.log(`Created directory: ${dirPath}`);
|
|
93
|
+
|
|
94
|
+
// Copy or reference images
|
|
95
|
+
const images: { filename: string; relativePath: string }[] = [];
|
|
96
|
+
|
|
97
|
+
for (const file of imageFiles) {
|
|
98
|
+
const srcPath = path.join(folderPath, file);
|
|
99
|
+
|
|
100
|
+
if (copyImages) {
|
|
101
|
+
const destPath = path.join(imagesDir, file);
|
|
102
|
+
fs.copyFileSync(srcPath, destPath);
|
|
103
|
+
images.push({ filename: file, relativePath: `./images/${file}` });
|
|
104
|
+
console.log(` Copied: ${file}`);
|
|
105
|
+
} else {
|
|
106
|
+
// Use absolute path from project root
|
|
107
|
+
const absoluteSrcPath = path.resolve(srcPath);
|
|
108
|
+
const relativePath = path.relative(dirPath, absoluteSrcPath);
|
|
109
|
+
images.push({ filename: file, relativePath });
|
|
110
|
+
console.log(` Referenced: ${file}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Generate markdown content
|
|
115
|
+
const imageMarkdown = images
|
|
116
|
+
.map((img, index) => ``)
|
|
117
|
+
.join('\n\n');
|
|
118
|
+
|
|
119
|
+
const content = `---
|
|
120
|
+
title: "${postTitle}"
|
|
121
|
+
date: "${date}"
|
|
122
|
+
excerpt: "A collection of ${images.length} images."
|
|
123
|
+
category: "Gallery"
|
|
124
|
+
tags: ["images", "gallery"]
|
|
125
|
+
authors: ["Amytis"]
|
|
126
|
+
layout: "post"
|
|
127
|
+
draft: false
|
|
128
|
+
latex: false
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
${imageMarkdown}
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
// Write markdown file
|
|
135
|
+
const targetPath = path.join(dirPath, 'index.mdx');
|
|
136
|
+
fs.writeFileSync(targetPath, content);
|
|
137
|
+
|
|
138
|
+
console.log(`\nCreated new post from images:`);
|
|
139
|
+
console.log(` Post: ${targetPath}`);
|
|
140
|
+
console.log(` Images: ${images.length} file(s)${copyImages ? ' (copied)' : ' (referenced)'}`);
|
|
141
|
+
console.log(`\nYou can edit the post at: ${dirPath}`);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pdf } from 'pdf-to-img';
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
// Parse arguments
|
|
8
|
+
const pdfPath = args.find(arg => arg.endsWith('.pdf') || (!arg.startsWith('--') && args.indexOf(arg) === 0));
|
|
9
|
+
const titleArgIndex = args.indexOf('--title');
|
|
10
|
+
const title = titleArgIndex > -1 ? args[titleArgIndex + 1] : '';
|
|
11
|
+
const scaleArg = args.indexOf('--scale');
|
|
12
|
+
const scale = scaleArg > -1 ? parseFloat(args[scaleArg + 1]) || 2.0 : 2.0;
|
|
13
|
+
|
|
14
|
+
if (!pdfPath || !fs.existsSync(pdfPath)) {
|
|
15
|
+
console.error('Please provide a valid PDF file path.');
|
|
16
|
+
console.error('Usage: bun run new-from-pdf <pdf-file> [--title "Post Title"] [--scale 2.0]');
|
|
17
|
+
console.error('');
|
|
18
|
+
console.error('Options:');
|
|
19
|
+
console.error(' --title Custom title for the post (default: PDF filename)');
|
|
20
|
+
console.error(' --scale Image scale factor (default: 2.0 for good quality)');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Generate slug from title or filename
|
|
25
|
+
const pdfBasename = path.basename(pdfPath, '.pdf');
|
|
26
|
+
const postTitle = title || pdfBasename.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
27
|
+
const slug = (title || pdfBasename)
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
30
|
+
.replace(/(^-|-$)+/g, '');
|
|
31
|
+
|
|
32
|
+
const date = new Date().toISOString().split('T')[0];
|
|
33
|
+
const dirName = `${date}-${slug}`;
|
|
34
|
+
const dirPath = path.join(process.cwd(), 'content', 'posts', dirName);
|
|
35
|
+
const imagesDir = path.join(dirPath, 'images');
|
|
36
|
+
|
|
37
|
+
// Check if post already exists
|
|
38
|
+
if (fs.existsSync(dirPath)) {
|
|
39
|
+
console.error(`Error: Post already exists at ${dirPath}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create directories
|
|
44
|
+
fs.mkdirSync(imagesDir, { recursive: true });
|
|
45
|
+
console.log(`Created directory: ${dirPath}`);
|
|
46
|
+
|
|
47
|
+
// Convert PDF to images
|
|
48
|
+
console.log(`Converting PDF (scale: ${scale})...`);
|
|
49
|
+
|
|
50
|
+
const images: string[] = [];
|
|
51
|
+
let pageNum = 0;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const pdfDocument = await pdf(pdfPath, { scale });
|
|
55
|
+
|
|
56
|
+
for await (const image of pdfDocument) {
|
|
57
|
+
pageNum++;
|
|
58
|
+
const filename = `page-${String(pageNum).padStart(3, '0')}.png`;
|
|
59
|
+
const imagePath = path.join(imagesDir, filename);
|
|
60
|
+
|
|
61
|
+
fs.writeFileSync(imagePath, image);
|
|
62
|
+
images.push(filename);
|
|
63
|
+
console.log(` Generated: ${filename}`);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
67
|
+
console.error(`Error converting PDF: ${errorMessage}`);
|
|
68
|
+
fs.rmSync(dirPath, { recursive: true });
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (images.length === 0) {
|
|
73
|
+
console.error('Error: No images were generated from the PDF.');
|
|
74
|
+
fs.rmSync(dirPath, { recursive: true });
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Generate markdown content
|
|
79
|
+
const imageMarkdown = images
|
|
80
|
+
.map((img, index) => ``)
|
|
81
|
+
.join('\n\n');
|
|
82
|
+
|
|
83
|
+
const content = `---
|
|
84
|
+
title: "${postTitle}"
|
|
85
|
+
date: "${date}"
|
|
86
|
+
excerpt: "Content extracted from PDF document."
|
|
87
|
+
category: "Document"
|
|
88
|
+
tags: ["pdf", "document"]
|
|
89
|
+
authors: ["Amytis"]
|
|
90
|
+
layout: "post"
|
|
91
|
+
draft: false
|
|
92
|
+
latex: false
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
${imageMarkdown}
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
// Write markdown file
|
|
99
|
+
const targetPath = path.join(dirPath, 'index.mdx');
|
|
100
|
+
fs.writeFileSync(targetPath, content);
|
|
101
|
+
|
|
102
|
+
console.log(`\nCreated new post from PDF:`);
|
|
103
|
+
console.log(` Post: ${targetPath}`);
|
|
104
|
+
console.log(` Images: ${images.length} page(s)`);
|
|
105
|
+
console.log(`\nYou can edit the post at: ${dirPath}`);
|