From 945bdaa6323267b33229cc4abb41bcc1ffc44225 Mon Sep 17 00:00:00 2001 From: wassfila Date: Sun, 23 Nov 2025 15:17:48 +0100 Subject: [PATCH 01/24] initial porting from astro-big-doc sqlite branch --- astro.config.mjs | 16 + config.js | 7 +- package.json | 3 +- pnpm-lock.yaml | 82 +++- public/.nojekyll | 0 public/favicon.ico | Bin 0 -> 118902 bytes public/menu.json | 354 ++++++++++++++ public/svg.svg | 17 + readme.md | 19 + server/.gitignore | 3 + server/auth/auth_router.js | 82 ++++ server/auth/auth_utils.js | 50 ++ server/bigdoc.service | 12 + server/create.sh | 2 + server/install.sh | 7 + server/server.js | 52 ++ src/assets/copy.svg | 15 + src/assets/discord.svg | 7 + src/assets/download.svg | 11 + src/assets/full-screen.svg | 19 + src/assets/fullscreen.drawio | 41 ++ src/assets/fullscreen.svg | 1 + src/assets/github-dark.png | Bin 0 -> 4691 bytes src/assets/github.svg | 11 + src/assets/hamburger.svg | 5 + src/assets/link.svg | 21 + src/assets/new.svg | 18 + src/assets/notes-caution.svg | 18 + src/assets/notes-danger.svg | 5 + src/assets/notes-note.svg | 18 + src/assets/notes-tip.svg | 8 + src/assets/pdf.svg | 14 + src/assets/rightarrow.svg | 3 + src/assets/search.png | Bin 0 -> 2915 bytes src/components/gallery/gallery.astro | 60 +++ src/components/gallery/gallery.js | 22 + src/components/gallery/gallery_pz.astro | 133 ++++++ src/components/gallery/gallery_pz.js | 42 ++ src/components/gallery/grid_utils.js | 72 +++ src/components/icon.astro | 13 + src/components/markdown/AstroMarkdown.astro | 96 ++++ src/components/markdown/Heading.astro | 78 +++ src/components/markdown/Link.astro | 72 +++ src/components/markdown/Tag.astro | 35 ++ src/components/markdown/cards/Cards.astro | 60 +++ src/components/markdown/cards/CardsMeta.astro | 123 +++++ src/components/markdown/code/Code.astro | 59 +++ .../markdown/code/DiagramCode.astro | 62 +++ .../markdown/code/Highlighter.astro | 105 ++++ src/components/markdown/code/Kroki.astro | 32 ++ src/components/markdown/code/LinkCode.astro | 32 ++ src/components/markdown/code/diagram.js | 24 + src/components/markdown/code/highlighter.js | 36 ++ src/components/markdown/code/kroki.yaml | 20 + .../markdown/directive/ButtonDirective.astro | 65 +++ .../directive/ContainerDirective.astro | 31 ++ .../markdown/directive/DetailsDirective.astro | 56 +++ .../markdown/directive/Directive.astro | 33 ++ .../markdown/directive/ImageDirective.astro | 39 ++ .../markdown/directive/NotesDirective.astro | 83 ++++ .../directive/OptimizedImageDirective.astro | 48 ++ src/components/markdown/heading.js | 36 ++ .../markdown/image/MarkdownImage.astro | 18 + .../markdown/model/ModelViewer.astro | 49 ++ .../markdown/model/ModelViewerCode.astro | 93 ++++ src/components/markdown/node_check.js | 29 ++ src/components/markdown/table/DataTable.astro | 37 ++ src/components/markdown/table/Table.astro | 27 ++ src/components/markdown/table/TableXLSX.astro | 52 ++ src/components/markdown/table/data-tables.js | 35 ++ src/components/markdown/table/table.js | 32 ++ src/components/panzoom/client_utils.js | 15 + src/components/panzoom/lib_panzoommodal.js | 214 +++++++++ src/components/panzoom/lib_svg_filters.js | 133 ++++++ src/components/panzoom/lib_svg_utils.js | 148 ++++++ src/components/panzoom/panzoom.astro | 108 +++++ src/components/panzoom/panzoom.js | 20 + src/components/panzoom/panzoom_common.js | 71 +++ src/components/panzoom/panzoommodal.astro | 86 ++++ src/components/svgicons.astro | 17 + src/components/swiper/swiper.astro | 86 ++++ src/components/swiper/swiper.js | 29 ++ src/env.d.ts | 2 + src/layout/AppBar.astro | 81 ++++ src/layout/Cache.astro | 19 + src/layout/ClientNavMenu.astro | 129 +++++ src/layout/Layout.astro | 217 +++++++++ src/layout/SideMenu.astro | 48 ++ src/layout/SubMenu.astro | 117 +++++ src/layout/cache.js | 20 + src/layout/client_nav_menu.js | 221 +++++++++ src/layout/colors.css | 18 + src/layout/layout_utils.js | 114 +++++ src/layout/menu_interactions_activation.js | 102 ++++ src/layout/toc_menu_activation.js | 56 +++ src/libs/assets.js | 211 ++++++++ src/libs/log.js | 47 ++ src/libs/redirect.js | 31 ++ src/libs/structure-db.js | 449 ++++++++++++++++++ src/libs/utils.js | 80 ++++ src/pages/404.astro | 9 + src/pages/[...url].astro | 17 + src/pages/api/redirect.js | 14 + src/pages/assets/[...path].js | 34 ++ src/pages/codes/[...path].js | 49 ++ src/pages/index.astro | 19 + tsconfig.json | 14 + 107 files changed, 5897 insertions(+), 8 deletions(-) create mode 100644 astro.config.mjs create mode 100644 public/.nojekyll create mode 100644 public/favicon.ico create mode 100644 public/menu.json create mode 100644 public/svg.svg create mode 100644 server/.gitignore create mode 100644 server/auth/auth_router.js create mode 100644 server/auth/auth_utils.js create mode 100644 server/bigdoc.service create mode 100644 server/create.sh create mode 100644 server/install.sh create mode 100644 server/server.js create mode 100644 src/assets/copy.svg create mode 100644 src/assets/discord.svg create mode 100644 src/assets/download.svg create mode 100644 src/assets/full-screen.svg create mode 100644 src/assets/fullscreen.drawio create mode 100644 src/assets/fullscreen.svg create mode 100644 src/assets/github-dark.png create mode 100644 src/assets/github.svg create mode 100644 src/assets/hamburger.svg create mode 100644 src/assets/link.svg create mode 100644 src/assets/new.svg create mode 100644 src/assets/notes-caution.svg create mode 100644 src/assets/notes-danger.svg create mode 100644 src/assets/notes-note.svg create mode 100644 src/assets/notes-tip.svg create mode 100644 src/assets/pdf.svg create mode 100644 src/assets/rightarrow.svg create mode 100644 src/assets/search.png create mode 100644 src/components/gallery/gallery.astro create mode 100644 src/components/gallery/gallery.js create mode 100644 src/components/gallery/gallery_pz.astro create mode 100644 src/components/gallery/gallery_pz.js create mode 100644 src/components/gallery/grid_utils.js create mode 100644 src/components/icon.astro create mode 100644 src/components/markdown/AstroMarkdown.astro create mode 100644 src/components/markdown/Heading.astro create mode 100644 src/components/markdown/Link.astro create mode 100644 src/components/markdown/Tag.astro create mode 100644 src/components/markdown/cards/Cards.astro create mode 100644 src/components/markdown/cards/CardsMeta.astro create mode 100644 src/components/markdown/code/Code.astro create mode 100644 src/components/markdown/code/DiagramCode.astro create mode 100644 src/components/markdown/code/Highlighter.astro create mode 100644 src/components/markdown/code/Kroki.astro create mode 100644 src/components/markdown/code/LinkCode.astro create mode 100644 src/components/markdown/code/diagram.js create mode 100644 src/components/markdown/code/highlighter.js create mode 100644 src/components/markdown/code/kroki.yaml create mode 100644 src/components/markdown/directive/ButtonDirective.astro create mode 100644 src/components/markdown/directive/ContainerDirective.astro create mode 100644 src/components/markdown/directive/DetailsDirective.astro create mode 100644 src/components/markdown/directive/Directive.astro create mode 100644 src/components/markdown/directive/ImageDirective.astro create mode 100644 src/components/markdown/directive/NotesDirective.astro create mode 100644 src/components/markdown/directive/OptimizedImageDirective.astro create mode 100644 src/components/markdown/heading.js create mode 100644 src/components/markdown/image/MarkdownImage.astro create mode 100644 src/components/markdown/model/ModelViewer.astro create mode 100644 src/components/markdown/model/ModelViewerCode.astro create mode 100644 src/components/markdown/node_check.js create mode 100644 src/components/markdown/table/DataTable.astro create mode 100644 src/components/markdown/table/Table.astro create mode 100644 src/components/markdown/table/TableXLSX.astro create mode 100644 src/components/markdown/table/data-tables.js create mode 100644 src/components/markdown/table/table.js create mode 100644 src/components/panzoom/client_utils.js create mode 100644 src/components/panzoom/lib_panzoommodal.js create mode 100644 src/components/panzoom/lib_svg_filters.js create mode 100644 src/components/panzoom/lib_svg_utils.js create mode 100644 src/components/panzoom/panzoom.astro create mode 100644 src/components/panzoom/panzoom.js create mode 100644 src/components/panzoom/panzoom_common.js create mode 100644 src/components/panzoom/panzoommodal.astro create mode 100644 src/components/svgicons.astro create mode 100644 src/components/swiper/swiper.astro create mode 100644 src/components/swiper/swiper.js create mode 100644 src/env.d.ts create mode 100644 src/layout/AppBar.astro create mode 100644 src/layout/Cache.astro create mode 100644 src/layout/ClientNavMenu.astro create mode 100644 src/layout/Layout.astro create mode 100644 src/layout/SideMenu.astro create mode 100644 src/layout/SubMenu.astro create mode 100644 src/layout/cache.js create mode 100644 src/layout/client_nav_menu.js create mode 100644 src/layout/colors.css create mode 100644 src/layout/layout_utils.js create mode 100644 src/layout/menu_interactions_activation.js create mode 100644 src/layout/toc_menu_activation.js create mode 100644 src/libs/assets.js create mode 100644 src/libs/log.js create mode 100644 src/libs/redirect.js create mode 100644 src/libs/structure-db.js create mode 100644 src/libs/utils.js create mode 100644 src/pages/404.astro create mode 100644 src/pages/[...url].astro create mode 100644 src/pages/api/redirect.js create mode 100644 src/pages/assets/[...path].js create mode 100644 src/pages/codes/[...path].js create mode 100644 src/pages/index.astro create mode 100644 tsconfig.json diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..13ea264 --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,16 @@ +import { defineConfig } from 'astro/config'; +import node from '@astrojs/node'; +import {config} from './config.js' +import yaml from '@rollup/plugin-yaml'; + +export default defineConfig({ + adapter: node({ + mode: 'middleware', + }), + output: "server", + outDir: config.outDir, + trailingSlash: 'ignore', + vite: { + plugins: [yaml()] + } +}); diff --git a/config.js b/config.js index a6b10b4..2c558ce 100644 --- a/config.js +++ b/config.js @@ -1,16 +1,11 @@ import * as dotenv from 'dotenv' import {join} from 'path' -import { fileURLToPath } from "node:url"; import path from "node:path"; import fsp from "node:fs/promises"; import yaml from "js-yaml"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const ROOT_DIR = path.resolve(__dirname); - async function loadManifest() { - const manifestPath = path.join(ROOT_DIR, "manifest.yaml"); + const manifestPath = path.join(process.cwd(), "manifest.yaml"); const raw = await fsp.readFile(manifestPath, "utf8"); return yaml.load(raw); } diff --git a/package.json b/package.json index 4df0411..323baf3 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,12 @@ "collect": "node scripts/collect.js" }, "dependencies": { + "@astrojs/node": "^9.5.1", "@google/model-viewer": "^4.1.0", "@octokit/rest": "^21.0.2", "@svgdotjs/svg.js": "^3.2.4", "adm-zip": "^0.5.16", - "astro": "^5.0.3", + "astro": "^5.16.0", "content-structure": "2.0.1", "cookie-parser": "^1.4.7", "cors": "^2.8.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 968dbed..8d66661 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@astrojs/node': + specifier: ^9.5.1 + version: 9.5.1(astro@5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)) '@google/model-viewer': specifier: ^4.1.0 version: 4.1.0(three@0.172.0) @@ -21,7 +24,7 @@ importers: specifier: ^0.5.16 version: 0.5.16 astro: - specifier: ^5.0.3 + specifier: ^5.16.0 version: 5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3) content-structure: specifier: 2.0.1 @@ -123,6 +126,11 @@ packages: '@astrojs/markdown-remark@6.3.9': resolution: {integrity: sha512-hX2cLC/KW74Io1zIbn92kI482j9J7LleBLGCVU9EP3BeH5MVrnFawOnqD0t/q6D1Z+ZNeQG2gNKMslCcO36wng==} + '@astrojs/node@9.5.1': + resolution: {integrity: sha512-7k+SU877OUQylPr0mFcWrGvNuC78Lp9w+GInY8Rwc+LkHyDP9xls+nZAioK0WDWd+fyeQnlHbpDGURO3ZHuDVg==} + peerDependencies: + astro: ^5.14.3 + '@astrojs/prism@3.3.0': resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} @@ -1393,6 +1401,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -1497,6 +1509,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1807,10 +1823,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -2166,10 +2190,17 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + serve-static@1.16.2: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + server-destroy@1.0.1: + resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -2246,6 +2277,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2733,6 +2768,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@astrojs/node@9.5.1(astro@5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))': + dependencies: + '@astrojs/internal-helpers': 0.7.5 + astro: 5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3) + send: 1.2.0 + server-destroy: 1.0.1 + transitivePeerDependencies: + - supports-color + '@astrojs/prism@3.3.0': dependencies: prismjs: 1.30.0 @@ -3949,6 +3993,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fs-constants@1.0.0: {} fsevents@2.3.3: @@ -4127,6 +4173,14 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -4633,10 +4687,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mimic-response@3.1.0: {} @@ -5077,6 +5137,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serve-static@1.16.2: dependencies: encodeurl: 2.0.0 @@ -5086,6 +5162,8 @@ snapshots: transitivePeerDependencies: - supports-color + server-destroy@1.0.1: {} + setprototypeof@1.2.0: {} sharp@0.33.5: @@ -5219,6 +5297,8 @@ snapshots: statuses@2.0.1: {} + statuses@2.0.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 diff --git a/public/.nojekyll b/public/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0307b124f78bab8a088cbb90b4a2d1a89b5c73a8 GIT binary patch literal 118902 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 z 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 + +SVG Simple Icon Tiny +Designed for the SVG Logo Contest in 2006 by Harvey Rayner. It is available under the Creative Commons license for those who have an SVG product or who are using SVG on their site. + + + + + + + + + + + + + \ No newline at end of file diff --git a/readme.md b/readme.md index 8083762..d129649 100644 --- a/readme.md +++ b/readme.md @@ -43,3 +43,22 @@ True content based ISR (Incremental Static Regenration) with cache warmup. - `folders` pulls those subfolders and flattens their contents into `dest`; omit `folders` to copy the whole repo. `dest` defaults to the repo name and is cleared before copying. - Set `GITHUB_TOKEN` to avoid GitHub rate limits. - Run `pnpm fetch` (or `node scripts/fetch.js`) after installing dependencies. + +## collection +- All configs are optional and have defaults +- Configure `collect` in `manifest.yaml`. Example: + ```yaml + collect: + folder_single_doc: false + file_link_ext: ["svg","webp","png","jpeg","jpg","xlsx","glb"] + file_compress_ext: ['txt','md','json','csv','tsv','yaml','yml'] + external_storage_kb: 512 + inline_compression_kb: 32 + ``` + - `folder_single_doc` default is false for one document per file, when true, generates one document per folder merging its markdown files. + - `file_link_ext` : only these extensions will be considered as assets to manage + - `file_compress_ext` : files subject to compressions in blobs storage + - `external_storage_kb` : threshold to manage blobs in folders and not in db + - `inline_compression_kb` : threshold above which db blobs get compressed +- Run `pnpm collect` to parse the `.content` directory Markdown and referenced assets and store them in `.structure/structure.db` + diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..3751748 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,3 @@ +*.debug +cert +auth/session.json diff --git a/server/auth/auth_router.js b/server/auth/auth_router.js new file mode 100644 index 0000000..e17bd44 --- /dev/null +++ b/server/auth/auth_router.js @@ -0,0 +1,82 @@ +import passport from 'passport' +import {Strategy} from 'passport-github' +import express from 'express' +import session from 'express-session' +import { verifyUser } from './auth_utils.js' +import { env } from 'node:process'; + +import * as dotenv from 'dotenv' +dotenv.config() + +const GitHubStrategy = Strategy; + +const callbackURL = process.env.PROTOCOL+"://"+process.env.HOST+":"+process.env.PORT+"/auth/github/callback" +const strategyConfig = { + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: callbackURL +} +let first_url = '/' + + +passport.use(new GitHubStrategy(strategyConfig, verifyUser)); +passport.serializeUser((user,done)=>{done(null,user)}); +passport.deserializeUser((user,done)=>{done(null,user)}); + + +const sessionStore = new session.MemoryStore() +const sessionHandler = session({ + secret:process.env.SESSION_SECRET, + resave:false, + saveUninitialized:false, + // using MemoryStore for limited users pool only (accepting reset on server restart) + store: sessionStore +}) + +const storeUser = (req,res,next)=>{ + const user = req.session?.passport?.user + if(user){ + env[req.sessionID] = user.id + //TODO on session expires delte the sessionID to prevent a memory leak + } + next() +} + +const onSuccess = function(req, res) { + console.log(`req.sessionID : ${req.sessionID} ; authenticated : ${req.isAuthenticated()}`) + // Auth Success, redirect to first requested url + res.redirect(first_url); +} + +const checkAuthenticated = (req,res,next)=>{ + if(req.isAuthenticated()){ + next() + }else{ + console.log(`req.sessionID : ${req.sessionID} ; authenticated : ${req.isAuthenticated()}`) + first_url = req.url + res.redirect('/auth/github') + } +} + +const authRouter = express.Router() +//'sessionStore', 'sessionID', 'session' (session.cookie, session.passport.user) +//sessionStore => [ '_events', '_eventsCount', '_maxListeners', 'sessions', 'generate' ] +authRouter.use(sessionHandler) +//'logIn', 'login', 'logOut', 'logout', 'isAuthenticated', 'isUnauthenticated' +authRouter.use(passport.initialize()) +authRouter.use(passport.session()) +authRouter.use(storeUser) +authRouter.get('/auth/github', + passport.authenticate('github')); + +authRouter.get('/auth/github/callback', + passport.authenticate('github', { failureRedirect: '/access' }), + onSuccess); + +authRouter.use(checkAuthenticated) + +//TODO checkAuthorised + +export{ + authRouter +} diff --git a/server/auth/auth_utils.js b/server/auth/auth_utils.js new file mode 100644 index 0000000..4c32c80 --- /dev/null +++ b/server/auth/auth_utils.js @@ -0,0 +1,50 @@ +import { env } from 'node:process'; + +function showKeys(info,obj){ + console.log(info) + let list_keys = Object.keys(obj) + list_keys = list_keys.filter((item)=>(!item.startsWith('_'))) + console.log(list_keys) +} + +function verifyUser(accessToken, refreshToken, profile, cb){ + const user = profile + console.log(` * verifyUser(id:${user.id})`) + console.log(" checking user id, this is a demo, all users accepted ") + user.role = 'admin', + user.groups = ['Markdown','Astro','Blog','About'] + + //in case of real verification, check user in DB, otherwise return non null err + cb(null,user) +} + +function get_session_id(cookie){ + const prefix = "connect.sid=s%3A" + if(cookie){ + if(cookie.startsWith(prefix)){ + return (cookie.split(prefix)[1].split(".")[0]) + } + } + return 0 +} + +function session_user(request){ + if(request.user){ + return request.user + } + else if(import.meta.env.PROD){ + const cookie = request.headers.get('cookie'); + const session_id = get_session_id(cookie) + const user = env[session_id] //give back the user saved by storeUser + request.user = user + return user + } + else return "" +} + +export { + showKeys, + verifyUser, + get_session_id, + session_user +} diff --git a/server/bigdoc.service b/server/bigdoc.service new file mode 100644 index 0000000..f7e8985 --- /dev/null +++ b/server/bigdoc.service @@ -0,0 +1,12 @@ +[Unit] +Description=Astro big doc site template signin with github +After=multi-user.target + +[Service] +Type=simple +WorkingDirectory=/home/user/astro-big-doc +ExecStart=node /home/user/astro-big-doc/server/server.js +Restart=on-abort + +[Install] +WantedBy=multi-user.target diff --git a/server/create.sh b/server/create.sh new file mode 100644 index 0000000..d74a754 --- /dev/null +++ b/server/create.sh @@ -0,0 +1,2 @@ +openssl req -x509 -newkey rsa:2048 -keyout cert/keytmp.pem -out cert/cert.pem -days 365 +openssl rsa -in cert/keytmp.pem -out cert/key.pem diff --git a/server/install.sh b/server/install.sh new file mode 100644 index 0000000..3a4a991 --- /dev/null +++ b/server/install.sh @@ -0,0 +1,7 @@ +sudo cp bigdoc.service /lib/systemd/system/ +sudo chmod 644 /lib/systemd/system/bigdoc.service +sudo chmod +x server/server.js +sudo systemctl daemon-reload +sudo systemctl enable bigdoc.service +sudo systemctl start bigdoc.service +sudo systemctl status bigdoc.service diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..9a3b4a7 --- /dev/null +++ b/server/server.js @@ -0,0 +1,52 @@ +import express from 'express'; +import https from 'https' +import { fileURLToPath } from 'url'; +import { join, dirname } from 'path'; +import { readFileSync, } from 'fs'; +import { handler as ssrHandler } from '../dist/server/entry.mjs'; +import cors from 'cors'; + +import * as dotenv from 'dotenv' +dotenv.config() + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const outdir = (process.env.OUT_DIR==null)?"dist/client/":process.env.OUT_DIR +const protocol = (process.env.PROTOCOL==null)?"http":process.env.PROTOCOL +const host = (process.env.HOST==null)?"0.0.0.0":process.env.HOST +const port = (process.env.PORT==null)?"3001":process.env.PORT + +const app = express(); +if(process.env.ENABLE_CORS == "true"){ + app.use(cors()); + console.log("\n -- !!! CORS enabled !!! -- APIs can be used from other sites --\n") +} + +if(process.env.ENABLE_AUTH === "true"){ + const { authRouter } = await import('./auth/auth_router.js'); + app.use(authRouter) + console.log(" with auth") +}else{ + console.log("\n -- !!! no auth !!! -- Authentication is disabled -- \n") +} + +app.use(ssrHandler); +app.use(express.static(outdir)) + +app.use((req, res, next) => { + res.status(404).send("Sorry can't find that!") + }) + + +if(protocol == "https"){ + const key = readFileSync(join(__dirname, process.env.KEY_FILE),'utf8') + const cert = readFileSync(join(__dirname, process.env.CERT_FILE),'utf8') + const httpsServer = https.createServer({key,cert},app) + httpsServer.listen(port,host,()=>{ + console.log(`listening on ${protocol}://${host}:${port}`) + }); +}else{ + app.listen(port,host,()=>{ + console.log(`listening on ${protocol}://${host}:${port}`) + }); +} diff --git a/src/assets/copy.svg b/src/assets/copy.svg new file mode 100644 index 0000000..3ca7889 --- /dev/null +++ b/src/assets/copy.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/src/assets/discord.svg b/src/assets/discord.svg new file mode 100644 index 0000000..1b0d6f6 --- /dev/null +++ b/src/assets/discord.svg @@ -0,0 +1,7 @@ + + + diff --git a/src/assets/download.svg b/src/assets/download.svg new file mode 100644 index 0000000..e0109c2 --- /dev/null +++ b/src/assets/download.svg @@ -0,0 +1,11 @@ + + + + diff --git a/src/assets/full-screen.svg b/src/assets/full-screen.svg new file mode 100644 index 0000000..9d1e73e --- /dev/null +++ b/src/assets/full-screen.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/fullscreen.drawio b/src/assets/fullscreen.drawio new file mode 100644 index 0000000..d13dbb8 --- /dev/null +++ b/src/assets/fullscreen.drawio @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/fullscreen.svg b/src/assets/fullscreen.svg new file mode 100644 index 0000000..f3d810c --- /dev/null +++ b/src/assets/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/github-dark.png b/src/assets/github-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..23a5c56d36a0b433f652ad546a7d7323ec9885d6 GIT binary patch literal 4691 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 + + + diff --git a/src/assets/hamburger.svg b/src/assets/hamburger.svg new file mode 100644 index 0000000..2baf2d0 --- /dev/null +++ b/src/assets/hamburger.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/assets/link.svg b/src/assets/link.svg new file mode 100644 index 0000000..f7084cc --- /dev/null +++ b/src/assets/link.svg @@ -0,0 +1,21 @@ + + + + + diff --git a/src/assets/new.svg b/src/assets/new.svg new file mode 100644 index 0000000..bbef588 --- /dev/null +++ b/src/assets/new.svg @@ -0,0 +1,18 @@ + + + + diff --git a/src/assets/notes-caution.svg b/src/assets/notes-caution.svg new file mode 100644 index 0000000..8dfd1ab --- /dev/null +++ b/src/assets/notes-caution.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/notes-danger.svg b/src/assets/notes-danger.svg new file mode 100644 index 0000000..97c67db --- /dev/null +++ b/src/assets/notes-danger.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/assets/notes-note.svg b/src/assets/notes-note.svg new file mode 100644 index 0000000..0a9719c --- /dev/null +++ b/src/assets/notes-note.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/notes-tip.svg b/src/assets/notes-tip.svg new file mode 100644 index 0000000..38d8a1b --- /dev/null +++ b/src/assets/notes-tip.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/pdf.svg b/src/assets/pdf.svg new file mode 100644 index 0000000..19fea32 --- /dev/null +++ b/src/assets/pdf.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/src/assets/rightarrow.svg b/src/assets/rightarrow.svg new file mode 100644 index 0000000..d3320a7 --- /dev/null +++ b/src/assets/rightarrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/search.png b/src/assets/search.png new file mode 100644 index 0000000000000000000000000000000000000000..fa2ff6baef0de32350520c1d39769e0b36a4fcdf GIT binary patch literal 2915 zcmV-p3!L*zAAI{#G#N6Cxc6WAW_t_sj-*%ooGjn!k_nb55I3)sboTTZJPL(uO(iBOPB~8dL z*_Z$JO4=c5YyNxBIaj;9Z6V6Y0g`l@q`8tlFKLc!@+Tal*;wq6^n#>kC9Re8w4_a) zQuIb`qm)njY)O|%xgU}CjLm?1r`C5Vvkj;00hhhUWz)sz4HntZ|tFKKG;6L z6c~;=rTz>9%jx57q4eu9aHgdDWuJ9TWuv6;IOo>I7Nl;nq7AVn{|ZTK)a=QZ?2~j2 z@ICr$ODLoOzywJ*N?IYa*0q62lJ1stJ5cz+Xd2#I0A{jXF6oNu#Yu+WNcz5WZeMIc zLgS?X`Q)#VbZHoIbU@MvlHQQn`FBVflr$n~qRdAyAoCXtNIFg>wy|;egQUxxbAy5Y zKM8tPfPC3sA?fmpWq3G4- zZmaO~YY4axI3Ji;k&KgpZvu}2|Eo~eTj{eG!;b+hsAw>E1IvJrjvxWEfP1J$5r%;U zA>~aP;M@uZau|4+z!QHI+0O(XFDW}%|IevY_9OsK2DX(PylucmMEbM>CScycohAG6 z8Zb3RAEJQm`;V3!w5Nb+vHK!P!0D8CN#94m*a)zvrYOU}oh0UQ!(7y>O7`p9vHKAm zV1VN8++pBm5`9uIclEs`lwbqU7VGEH0q!m_`8TFI`J-U(1^1Ltg2l1>5De@SYJb6j zS(f_bkCH3F9}D(#Cva-)K2#3o;d`*)psc1MqfT7_Q-FUK?C-6y<*yvzG>Sd_n}LDY z@)rS^0calMs8pgzIOn!X`c;9Z zmsBlF?Et6XAJMyomKj8V6Y%#LY^TAejz+)P&zFB>&E3wqJr&Cn73bW0lJ4?sHdWHu z0mP|=(MkV`XS;op9;jHBRJcL(=mBWJ!sQ zk~VoZC$uOz_s|vLHLB+B} z2Rk71!valLS1d}s0LS5fAYwOga%|bF1ULi#12XS7H6T>VSAaak>rb9-Cri4va)~05 zKm9kz-$C|Q=iIL9C8`-LYo{+C0-DGj4i(pz)XrMJiwCJ@&^H11>P<=C>wxNV)vmJu#6y^3-v0BxY=)bPE6rNS#S`DnOVCgV{Y!kKK>j0In%9SYQft6_KA9%+$S@;wf-PSroG$wS#R1WzwsU@(_{y z2G~kgW;+{Y3l=HVUtpQD%Iuk=YzG!nQw;&&B;XFpZDU8$S4AZcf_cfxmRuQSGo}cY z#}#7veWqje3W_9s?1jEnf(xj&9NSK;aKnXIn%bgrW!TexIksJu-RFDZ4qBoFzLi5S zl60^9PZW>GURhY$^O9bYMc8hXg{<$D1zl#cQ%M7|v-r~`eMT0UbcX6b=lE+$KWchW z$s)CbFP9ZruH%JhL>90*D2qQxEgZe^O5m*klBHLdDM3<2p(1gYY%C7sKgm|DJ(Av& zv^NjDO_IWr0cT5kP*SVH!YXEuq;-;3N_xOKmt1z0QmP3|mv!#UYQ%}k2MW8z?2=_8 zzbU1PVfPd8>&wk))+NcE5!H}!lDOQ%PNL2;IW8I_-xussyozrhfotQ7qSTD29RS>6D;8uzWuDfvrbw*u5eVk^OjY!z77tpGKVuLSdyMvdW2(vPKX z1*jdYj>0NgqRW^JN;m()1jy6c%$M|R&9^?#DL^9RiKORC zdb-Bj?&=gEA)IqNBz;}d6E)p(Q)dWD5G;GzN=c#f34bZ@|92Dxm;^i=&^7!FU6fGT zVCiWd3Zww%#O_~54`5YKmsdRbOJnz|!vm}|-Yry + {imagesUrls.map((image)=>( + + {(image.ext == ".svg")? + : + + } + + ))} + + + + + diff --git a/src/components/markdown/code/Highlighter.astro b/src/components/markdown/code/Highlighter.astro new file mode 100644 index 0000000..836c96b --- /dev/null +++ b/src/components/markdown/code/Highlighter.astro @@ -0,0 +1,105 @@ +--- +import Svgicons from '@/components/svgicons.astro'; +import {codeToHtml} from './highlighter.js' + +export interface Props { + code: string; + language: string; + params: object; +} + +const { code, language, params } = Astro.props as Props; + +const {hash,html} = await codeToHtml(code, { lang: language, theme:'dark-plus' }) + +--- +
+ + + Copied! +
+ + + + + + diff --git a/src/components/markdown/code/Kroki.astro b/src/components/markdown/code/Kroki.astro new file mode 100644 index 0000000..ba0eef1 --- /dev/null +++ b/src/components/markdown/code/Kroki.astro @@ -0,0 +1,32 @@ +--- +import {config} from '@/config.js' +import { diagram_cache } from './diagram.js' +import DiagramCode from './DiagramCode.astro' + +export interface Props { + language: string; + code: string; + params: object; + meta: object; +} + +const { language, code, params, meta } = Astro.props as Props; + +async function generator(code){ + const response = await fetch(`${config.kroki_server}/${language}/svg/`,{ + method: 'POST', + body: code, + headers: { + 'Content-Type': 'text/plain', + }, + }) + + const svg_text = await response.text() + return svg_text +} + +const hash = await diagram_cache(code, generator) +console.log(` => kroki '${hash}'`) + +--- + diff --git a/src/components/markdown/code/LinkCode.astro b/src/components/markdown/code/LinkCode.astro new file mode 100644 index 0000000..051f6eb --- /dev/null +++ b/src/components/markdown/code/LinkCode.astro @@ -0,0 +1,32 @@ +--- +import Kroki from './Kroki.astro' +import kroki from './kroki.yaml' +import {join} from 'path' +import {config} from '@/config.js' +import {readFile} from 'fs/promises' +import {getMetaData} from '@/libs/assets.js' + +export interface Props { + ext: string; + url: string; + dirpath: string; +} + +const { ext, url, dirpath } = Astro.props as Props; + +let language = "" +let code = "" +const params = [] +let meta = {} + +const is_kroki = Object.keys(kroki.formats).includes(ext) +if(is_kroki){ + language = kroki.formats[ext] + const abs_file = join(config.content_path,dirpath,url) + code = await readFile(abs_file,'utf-8') + meta = await getMetaData(url, dirpath) +} +--- +{(is_kroki)&& + +} diff --git a/src/components/markdown/code/diagram.js b/src/components/markdown/code/diagram.js new file mode 100644 index 0000000..30d223e --- /dev/null +++ b/src/components/markdown/code/diagram.js @@ -0,0 +1,24 @@ +import {config} from '@/config.js' +import {exists, save_file, shortMD5} from '@/libs/utils.js' +import {join} from 'path' + +async function diagram_cache(code,generator){ + const hash = shortMD5(code) + const file_path = join(config.code_path,hash,"diagram.svg") + const file_exists = await exists(file_path) + if(file_exists){ + console.log(`* returning diagram from cache '${file_path}'`) + return hash + }else{ + console.log(`* generating diagram as not in cache`) + const svg_text = await generator(code) + await save_file(file_path,svg_text) + const code_path = join(config.code_path,hash,"code.txt") + await save_file(code_path,code) + return hash + } +} + +export{ + diagram_cache +} diff --git a/src/components/markdown/code/highlighter.js b/src/components/markdown/code/highlighter.js new file mode 100644 index 0000000..1211014 --- /dev/null +++ b/src/components/markdown/code/highlighter.js @@ -0,0 +1,36 @@ +import {config} from '@/config.js' +import {join} from 'path' +import {bundledLanguages, createHighlighter} from 'shiki'; +import { exists,save_file, shortMD5 } from '@/libs/utils.js'; + +const highlighter = await createHighlighter({ + themes:[config.highlighter.theme], + langs:config.highlighter.langs, +}) +await highlighter.loadTheme(config.highlighter.theme) + +async function codeToHtml(code, highlighter_config){ + const requested_language = highlighter_config.lang + let lang = requested_language + if(!Object.keys(bundledLanguages).includes(requested_language)){ + console.warn(` (X) ${requested_language} is not available, fall back on js`) + lang = 'js' + } + if (!highlighter.getLoadedLanguages().includes(lang)) { + await highlighter.loadLanguage(lang) + } + + + const html = highlighter.codeToHtml(code, { lang: lang, theme:config.highlighter.theme }) + const hash = shortMD5(code) + const file_path = join(config.code_path,hash,"code.txt") + //persist for highlighter copy, for code not saved by a diag gen + if(!await exists(file_path)){ + await save_file(file_path,code) + } + return {hash,html} +} + +export{ + codeToHtml +} diff --git a/src/components/markdown/code/kroki.yaml b/src/components/markdown/code/kroki.yaml new file mode 100644 index 0000000..3fc9c63 --- /dev/null +++ b/src/components/markdown/code/kroki.yaml @@ -0,0 +1,20 @@ +languages: + - graphviz + - nwdiag + - packetdiag + - bytefield + - wavedrom + - rackdiag + - wireviz + - vegalite + - mermaid + - blockdiag + - seqdiag + - actdiag + - pikchr + - pikchr + - erd + - nomnoml + - plantuml +formats: + puml: plantuml diff --git a/src/components/markdown/directive/ButtonDirective.astro b/src/components/markdown/directive/ButtonDirective.astro new file mode 100644 index 0000000..dbda684 --- /dev/null +++ b/src/components/markdown/directive/ButtonDirective.astro @@ -0,0 +1,65 @@ +--- +import Svgicons from '@/components/svgicons.astro'; +import {extname} from 'path' + +export interface Props { + label: string; + link: string; + icon: string; +} + +const { label, link, icon=null } = Astro.props as Props; +const external = link.startsWith('http') +let used_icon = icon + +if(icon == null){ + if(external){ + used_icon = "new" + }else if([".zip",".hex"].includes(extname(link))){ + used_icon = "download" + } +} +--- + + + diff --git a/src/components/markdown/directive/ContainerDirective.astro b/src/components/markdown/directive/ContainerDirective.astro new file mode 100644 index 0000000..4da9beb --- /dev/null +++ b/src/components/markdown/directive/ContainerDirective.astro @@ -0,0 +1,31 @@ +--- +import NotesDirective from "./NotesDirective.astro" +import DetailsDirective from "./DetailsDirective.astro" +export interface Props { + name: string; //note, tip, caution, danger + attributes: object; +} + +const {name, attributes} = Astro.props as Props; +const className = name.toLowerCase() +let typeName = "other" +if(["note","tip","caution","danger"].includes(className)){ + typeName = "notes" +}else if(className == "details"){ + typeName = "details" +} +--- +{(typeName == "notes")&& + + + +} +{(typeName == "details")&& + + + +} +{(typeName == "other")&& +

{className}

+ +} diff --git a/src/components/markdown/directive/DetailsDirective.astro b/src/components/markdown/directive/DetailsDirective.astro new file mode 100644 index 0000000..9f5774f --- /dev/null +++ b/src/components/markdown/directive/DetailsDirective.astro @@ -0,0 +1,56 @@ +--- +import Svgicons from '@/components/svgicons.astro'; +export interface Props { + attributes: object; +} + +const {attributes} = Astro.props as Props; +const summary = Object.hasOwn(attributes,"summary")?attributes.summary:"Details..." + +--- +
+ + {summary} + + +
+ +
+
+ + diff --git a/src/components/markdown/directive/Directive.astro b/src/components/markdown/directive/Directive.astro new file mode 100644 index 0000000..226ba7f --- /dev/null +++ b/src/components/markdown/directive/Directive.astro @@ -0,0 +1,33 @@ +--- +//to use Optimized Images, do the following : +// - uncomment import line with OptimizedImageDirective.astro +// - set config.copy_assets = true +// - set assets_hash_dir = true + +import ImageDirective from './ImageDirective.astro'; +//import ImageDirective from './OptimizedImageDirective.astro'; +import ButtonDirective from './ButtonDirective.astro' + +export interface Props { + node: object; + dirpath: string; +} + +const { node, dirpath} = Astro.props as Props; + +const is_image = (node.name == "image") +const is_button = (node.name == "button") +const is_other = !(is_image || is_button) + +--- +{is_image && + +} +{is_button && + +} +{is_other && +
{node.name} + {Object.keys(node.attributes).map((key)=>({key} = {node.attributes[key]}))} +
+} diff --git a/src/components/markdown/directive/ImageDirective.astro b/src/components/markdown/directive/ImageDirective.astro new file mode 100644 index 0000000..381030b --- /dev/null +++ b/src/components/markdown/directive/ImageDirective.astro @@ -0,0 +1,39 @@ +--- +import {assetToUrl} from '@/libs/assets.js' +export interface Props { + node: object; + dirpath: string; +} + +const { node, dirpath } = Astro.props as Props; + +const asseturl = await assetToUrl(node.attributes.src, dirpath) + +const alt = node.attributes.alt + +const title = Object.hasOwn(node.attributes,"title")?node.attributes.title:alt +let style = "" +if(node.attributes.width){ + style += `width:${node.attributes.width}px;` +} +if(node.attributes.height){ + style += `height:${node.attributes.height}px;` +} + +let add_class = "" +if(Object.hasOwn(node.attributes,"center")){ + add_class = "center" +} +--- +{alt} + + diff --git a/src/components/markdown/directive/NotesDirective.astro b/src/components/markdown/directive/NotesDirective.astro new file mode 100644 index 0000000..d221887 --- /dev/null +++ b/src/components/markdown/directive/NotesDirective.astro @@ -0,0 +1,83 @@ +--- +import Svgicons from '@/components/svgicons.astro'; +export interface Props { + name: string; //note, tip, caution, danger + attributes: object; +} + +const {name, attributes} = Astro.props as Props; +const title = Object.hasOwn(attributes,"title")?attributes.title:name +const className = name.toLowerCase() +--- + + + diff --git a/src/components/markdown/directive/OptimizedImageDirective.astro b/src/components/markdown/directive/OptimizedImageDirective.astro new file mode 100644 index 0000000..484ff9a --- /dev/null +++ b/src/components/markdown/directive/OptimizedImageDirective.astro @@ -0,0 +1,48 @@ +--- +import {assetToUrl} from '@/libs/assets.js' +import { Image } from "astro:assets" +import sharp from 'sharp'; +import {relAssetToPath} from '@/libs/assets.js' + +export interface Props { + node: object; + dirpath: string; +} + +const { node, dirpath } = Astro.props as Props; + +const asseturl = await assetToUrl(node.attributes.src, dirpath) + +const alt = node.attributes.alt?node.attributes.alt:node.attributes.src + +const image_path = relAssetToPath(node.attributes.src, dirpath) + +const image = sharp(image_path); +const metadata = await image.metadata(); + +const optionalWidth = node.attributes.width; +const optionalHeight = node.attributes.height; + +let width = metadata.width +let height = metadata.height +if(optionalWidth && !optionalHeight) { + width = optionalWidth; + height = Math.round((metadata.height / metadata.width) * optionalWidth); +} else if (!optionalWidth && optionalHeight) { + height = optionalHeight; + width = Math.round((metadata.width / metadata.height) * optionalHeight); +} else if(optionalWidth && optionalHeight) { + width = optionalWidth + height = optionalHeight +} + +const imported_image_src = { + src:asseturl, + width:width, + height:height, + format:metadata.format +} + +const title = Object.hasOwn(node.attributes,"title")?node.attributes.title:alt +--- +{alt} diff --git a/src/components/markdown/heading.js b/src/components/markdown/heading.js new file mode 100644 index 0000000..06647df --- /dev/null +++ b/src/components/markdown/heading.js @@ -0,0 +1,36 @@ + +function init(){ + const copyIcons = document.querySelectorAll('.icon.copy'); + copyIcons.forEach(icon => { + icon.addEventListener('click', function() { + // Copy data-sid to clipboard + const sid = this.getAttribute('data-sid'); + const url = `${window.location.origin}/${sid}`; + navigator.clipboard.writeText(url).then(() => { + // Show success message + const message = document.createElement('span'); + message.textContent = 'Copied!'; + message.style.position = 'absolute'; + message.style.left = '100%'; + message.style.top = '0'; + //message.style.backgroundColor = 'white'; + message.style.color = 'white'; + //message.style.border = '1px solid black'; + message.style.padding = '2px 8px'; + message.style.fontSize = '0.75rem'; + message.style.marginLeft = '10px'; + this.appendChild(message); + + // Remove the message after 1 second + setTimeout(() => { + this.removeChild(message); + }, 1000); + }).catch(err => { + console.error('Failed to copy text: ', err); + }); + }); + }); +} + +document.addEventListener('DOMContentLoaded', init); + diff --git a/src/components/markdown/image/MarkdownImage.astro b/src/components/markdown/image/MarkdownImage.astro new file mode 100644 index 0000000..876874e --- /dev/null +++ b/src/components/markdown/image/MarkdownImage.astro @@ -0,0 +1,18 @@ +--- +import {assetToUrl, getMetaData} from '@/libs/assets.js' +import Panzoom from '@/components/panzoom/panzoom.astro' + +export interface Props { + node: object; + dirpath: string; +} + +const { node, dirpath} = Astro.props as Props; +const {title, url, alt} = node; + +console.log(`url / dirpath = ${url} / ${dirpath}`) +const asseturl = await assetToUrl(url, dirpath) +const meta = await getMetaData(url, dirpath) +console.log(`asseturl = ${asseturl}`) +--- + diff --git a/src/components/markdown/model/ModelViewer.astro b/src/components/markdown/model/ModelViewer.astro new file mode 100644 index 0000000..4f5432e --- /dev/null +++ b/src/components/markdown/model/ModelViewer.astro @@ -0,0 +1,49 @@ +--- +import SvgIcons from '@/components/svgicons.astro' + +export interface Props { + src: string; + title: string; +} + +const { src, title} = Astro.props as Props; + + +--- +
+
{title}
+ + + +
+ + + + + + diff --git a/src/components/markdown/model/ModelViewerCode.astro b/src/components/markdown/model/ModelViewerCode.astro new file mode 100644 index 0000000..3f0bcfb --- /dev/null +++ b/src/components/markdown/model/ModelViewerCode.astro @@ -0,0 +1,93 @@ +--- +import SvgIcons from '@/components/svgicons.astro' +import yaml from 'js-yaml' +import {assetToUrl} from '@/libs/assets.js' + +export interface Props { + code: string; + dirpath: string; +} + +const { code, dirpath } = Astro.props as Props; + +const data = yaml.load(code) +let should_render = true +let src = "" +let title = "" +let poster = "" +let environment_image = "" +let error_text = "" +//mandatory params +if(Object.hasOwn(data,"src")){ + src = await assetToUrl(data.src,dirpath) +}else{ + should_render = false + const err = "ModelViewerCode missing 'src' param ; " + console.warn(err) + error_text += err +} +if(Object.hasOwn(data,"title")){ + title = data.title +}else{ + should_render = false + const err = "ModelViewerCode missing 'title' param" + console.warn(err) + error_text += err +} +//optional params +if(Object.hasOwn(data,"poster")){ + poster = await assetToUrl(data.poster,dirpath) +} +if(Object.hasOwn(data,"environment-image")){ + environment_image = await assetToUrl(data["environment-image"],dirpath) +} + +//potential additional params +//camera-orbit="-10.92deg 80.06deg 58.4m" +//field-of-view="30deg" + +--- +{should_render && +
+
{title}
+ + + +
+ + +} +{!should_render && +
{error_text}
+} + + + + diff --git a/src/components/markdown/node_check.js b/src/components/markdown/node_check.js new file mode 100644 index 0000000..8e88162 --- /dev/null +++ b/src/components/markdown/node_check.js @@ -0,0 +1,29 @@ + +function text_only_children(node){ + if(!node.children || node.children.length === 0){ + return false + } + for(const child of node.children){ + if(!["text","link"].includes(child.type)){ + return false + } + } + return true +} + +function has_image(node){ + if(!node.children || node.children.length === 0){ + return false + } + for(const child of node.children){ + if(child.type == "image"){ + return true + } + } + return false +} + +export{ + text_only_children, + has_image +} diff --git a/src/components/markdown/table/DataTable.astro b/src/components/markdown/table/DataTable.astro new file mode 100644 index 0000000..de635de --- /dev/null +++ b/src/components/markdown/table/DataTable.astro @@ -0,0 +1,37 @@ +--- +import {astToDataTable} from './table.js' + +export interface Props { + node: object; +} + +const { node } = Astro.props as Props; + +const data = astToDataTable(node) +const [table_head, ...table_rows] = data; +const table_string = JSON.stringify(table_rows) +--- +
+ + + + {table_head.map((cell)=>( + + ))} + + + + + +
{cell}
+
+ + + +