markopress 0.0.12 ā 0.0.13
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/dist/build/index.js +1 -1
- package/dist/config/loader.js +1 -1
- package/dist/config/types.d.ts +10 -1
- package/dist/config/validation.d.ts +16 -3
- package/dist/config/validation.js +1 -1
- package/dist/dev/index.js +1 -1
- package/dist/plugin/manager.d.ts +4 -0
- package/dist/plugin/manager.js +1 -1
- package/dist/plugins/head-inject/index.d.ts +36 -0
- package/dist/plugins/head-inject/index.js +1 -0
- package/dist/plugins/head-inject/transformer.d.ts +13 -0
- package/dist/plugins/head-inject/transformer.js +1 -0
- package/dist/plugins/head-inject/types.d.ts +57 -0
- package/dist/plugins/head-inject/types.js +1 -0
- package/dist/plugins/head-inject/validator.d.ts +3 -0
- package/dist/plugins/head-inject/validator.js +1 -0
- package/dist/preview/index.js +1 -1
- package/package.json +1 -1
- package/src/theme/default/routes/+layout.marko +88 -0
- package/src/theme/default/tags/theme-head-bottom.marko +2 -1
- package/src/theme/default/tags/theme-head-top.marko +2 -3
package/dist/build/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{promises as e}from"node:fs";import o from"node:path";import{spawn as t}from"node:child_process";import{fileURLToPath as n}from"node:url";import s from"gray-matter";import{loadConfig as a}from"../config/loader.js";import{getDesignSystem as r,getDarkModeOverride as i}from"../theme/default/design-systems/index.js";import{globalTagValidator as c,formatValidationError as l}from"../markdown/index.js";import{PluginManager as d}from"../plugin/manager.js";import{loadMarkdownModule as u,registerMarkdownContent as m,escapeMarkoText as g}from"./vite-markdown-plugin.js";import{renderMarkdown as f}from"../markdown/renderer.js";import{buildSearchIndex as p}from"../search/index.js";const h=o.dirname(n(import.meta.url)),w=o.resolve(h,"..",".."),k=o.join(w,"src","theme","default"),y=new Set(["@markopress/theme-default","theme-default","default"]);function j(e){return y.has(e)}export function filePathToUrl(e,t){const n=o.relative(t,e).replace(/\.md$/,"").split(o.sep).join("/");return"index"===n?"/":n.endsWith("/index")?"/"+n.replace("/index",""):"/"+n}export async function build(n={}){const{outDir:r,debug:i=!1,useCatchAllRoutes:u,root:m}=n,h=m||process.cwd(),w=[],k=new Map,y=new Map,j=o.join(h,".markopress");let T=h;try{(await e.stat(j)).isDirectory()&&(T=j)}catch{}const v=e=>({start:()=>{y.set(e,performance.now())},end:()=>{const o=y.get(e)||0,t=performance.now()-o;k.set(e,t)}});try{console.log("š Building MarkoPress site...\n");const n=v("Config loading");n.start();const m=await a(h,{mode:"production",command:"build"});let y;if(n.end(),m.plugins&&m.plugins.length>0){console.log("š Loading plugins...");const e=v("Plugin loading");e.start(),y=new d(m),await y.loadPlugins(m.plugins),e.end(),console.log("")}if(y){console.log("š¦ Loading plugin content...");const e=v("Plugin loadContent hooks");e.start(),await y.execLoadContentHooks(),e.end(),console.log(" Plugin content loaded\n")}const j={},$=[],P=o.resolve(h,m.contentDir);try{const t=await e.readdir(P,{withFileTypes:!0,recursive:!0}),n=new Map;for(const a of t){if(!a.isFile()||!a.name.endsWith(".md"))continue;const t=o.join(a.path||a.parentPath||P,a.name),r=o.relative(P,t),i=filePathToUrl(t,P),c=r.split(o.sep),l=1===c.length?"root":c[0],d=await e.readFile(t,"utf-8");let u={};try{u=s(d).data}catch{}n.has(l)||n.set(l,[]),n.get(l).push({id:a.name.replace(".md",""),slug:a.name.replace(".md",""),filePath:t,urlPath:i,directory:l,processed:{frontmatter:u}})}for(const[e,t]of n){const n=m.content[e]||{},s=new Map;$.push({id:e,dir:o.join(P,"root"===e?"":e),config:n,features:n,files:t,enhance(e,o){s.set(e,o)},getEnhancement:e=>s.get(e),_enhancements:s})}}catch(e){console.warn("Warning: Could not scan content directory: "+e)}if(y&&$.length>0){console.log("š Enhancing modules with plugin metadata...");const t=v("Module enhancement");t.start();for(const e of $)console.log(` Module: ${e.id} (${e.files.length} files)`),e.files.length>0&&console.log(" First file has processed: "+!!e.files[0].processed);await y.execEnhanceModulesHooks($),t.end(),console.log(` Enhanced ${$.length} module(s)\n`);const n=o.join(T,"src",".generated");await e.mkdir(n,{recursive:!0});const s=(m.site?.base||"/").replace(/\/$/,""),a={};for(const e of $){const o={},t=e._enhancements.entries();for(const[e,n]of t)o[e]=n;Object.keys(o).length>0&&(s&&b(o,s),a[e.id]=o)}const r=o.join(n,"module-enhancements.js"),i=`// Auto-generated by MarkoPress - Do not edit\nexport default ${JSON.stringify(a,null,2)};\n`;await e.writeFile(r,i,"utf-8"),console.log(" Wrote module enhancements to src/.generated/module-enhancements.js\n")}if(!1!==m.search?.enabled){console.log("š Building search index...");const t=v("Search index");t.start();const n=[],s=(m.site?.base||"/").replace(/\/$/,"");for(const o of $)for(const t of o.files)try{const o=await e.readFile(t.filePath,"utf-8"),a=await f(o,{...m.markdown,base:s});n.push({url:t.urlPath,html:a.html,title:a.frontmatter?.title||t.id,frontmatter:a.frontmatter})}catch(e){console.warn(` Warning: Could not index ${t.filePath}:`,e)}try{const t=await p(n,m.search),s=o.join(T,"public","search-index.json");await e.mkdir(o.dirname(s),{recursive:!0}),await e.writeFile(s,t),console.log(` Search index built (${n.length} pages)\n`)}catch(e){console.warn(" Warning: Failed to build search index:",e)}t.end()}if(m.markdown.markoTags?.enabled){const e=o.join(T,m.markdown.markoTags.tagsDir||"src/tags");console.log("š Scanning tags directory...");const t=v("Tag validation setup");t.start(),await c.loadAvailableTags(e),t.end(),console.log(` Found ${c.getAvailableTagsCount()} tags\n`)}else c.reset();const C=o.join(T,"src","routes");await e.mkdir(C,{recursive:!0});let F={};if(y){const e=v("Extend routes hooks");e.start(),F=await y.execExtendRoutesHooks(F),e.end(),console.log("š Extended routes manifest:",Object.keys(F).length)}console.log("š Generating routes from content...");const S=v("Route generation");S.start();const x=u??m.build.useCatchAllRoutes;console.log("š Pre-rendering markdown to .marko files...");const E=v("Pre-render markdown");E.start();const _=o.join(T,"src",".generated","markdown");await e.mkdir(_,{recursive:!0});const D={};let A=0;for(const t of $){const n=o.join(_,t.id);await e.mkdir(n,{recursive:!0});for(const s of t.files)try{const a=await e.readFile(s.filePath,"utf-8"),r=(m.site?.base||"/").replace(/\/$/,""),i=await f(a,{base:r});let c=i.html.replace(/<span class="line"><\/span>(\s*<\/code>)/g,"$1");const l=`<div class="markdown-content">\n${g(c)}\n</div>`,d=o.join(n,s.slug+".marko");await e.writeFile(d,l),D[`${t.id}/${s.slug}`]={frontmatter:i.frontmatter,headers:i.headers||[]},A++}catch(e){console.warn(` Warning: Failed to pre-render ${t.id}/${s.slug}:`,e)}}const N=o.join(T,"src",".generated","content-metadata.js"),M=`// Auto-generated by MarkoPress - Do not edit\nexport default ${JSON.stringify(D,null,2)};\n`;await e.writeFile(N,M),E.end(),console.log(` Pre-rendered ${A} markdown files\n`),x?(await generateCatchAllRoutes(j,C,m,$,i,!0),console.log(" Using catch-all dynamic routes")):(await generateRoutes(j,C,m,$,i),console.log(" Using static routes")),S.end(),console.log(" Routes generated\n");const O=[];for(const[e,o]of Object.entries(F))(o.handler||o.component)&&(O.push({path:e,...o}),console.log(" Found plugin route: "+e));if(console.log(`š Total manifest routes: ${Object.keys(F).length}, Plugin routes: ${O.length}`),y){const t=[...y.getPluginRoutes(),...O];if(t.length>0){console.log(`š Generating ${t.length} plugin routes...`);const n=v("Plugin route generation");n.start(),await async function(t,n,s,a){for(const s of t){const t=s.path.slice(1),r=o.join(n,t,"+page");if(await e.mkdir(o.dirname(r),{recursive:!0}),s.handler){const t=o.join(o.dirname(r),"+handler.js");await e.writeFile(t,s.handler)}if(s.component){const o=r+".marko";await e.writeFile(o,s.component)}a&&console.log(" Generated plugin route: "+s.path)}}(t,C,0,i),n.end(),console.log(" Plugin routes generated\n")}}console.log("āļø Generating Vite config...");const I=v("Vite config generation");if(I.start(),await generateViteConfig(T,i),I.end(),console.log(" Vite config generated\n"),y){console.log("š Processing plugin allContentLoaded hooks...");const e=v("AllContentLoaded hooks");e.start(),await y.execAllContentLoadedHooks(F),e.end(),console.log(" All content processed\n")}if(m.markdown.markoTags?.enabled){console.log("š Validating Marko tags...");const e=v("Tag validation");e.start();const o=c.validate();if(e.end(),!o.success){const e=l(o.missingTags);return console.error(`\n${e}\n`),w.push(e),{success:!1,outDir:"",pages:0,errors:w}}console.log(" All tags validated ā\n")}console.log("šØ Copying theme CSS...");const L=v("Theme CSS copy");L.start(),await copyThemeCSS(T,m,i),L.end(),console.log(" Theme CSS copied\n"),console.log("šØ Extracting styles from Marko components...");const W=v("Marko component styles extraction");W.start(),await extractStylesFromMarkoTags(T,m,i),W.end(),console.log(" Component styles extracted\n");const R=[];for(const e of $)for(const o of e.files)R.push(o.urlPath);for(const e of Object.keys(F))R.includes(e)||R.push(e);const G=o.join(T,"src",".generated","static-urls.json");await e.mkdir(o.dirname(G),{recursive:!0}),await e.writeFile(G,JSON.stringify(R,null,2)),i&&console.log(` Generated static URL manifest: ${R.length} URLs`),console.log("šØ Building with @marko/run...");const U=v("@marko/run build");U.start();const B=r||m.build.outDir,V=await async function(e,n,s){return new Promise(a=>{const r=["build"];e&&r.push("--output",e),n&&r.push("--debug");const i=t("npx",["marko-run",...r],{stdio:"inherit",cwd:s});i.on("close",t=>{if(0===t){const t=e||"dist";a({success:!0,outDir:o.join(s,t),errors:[]})}else a({success:!1,outDir:"",errors:["Build process exited with code "+t]})}),i.on("error",e=>{a({success:!1,outDir:"",errors:["Failed to start build process: "+e.message]})})})}(B,i,T);if(U.end(),!V.success)return w.push(...V.errors),{success:!1,outDir:"",pages:0,errors:w};const H=v("Collect build assets");H.start();const Y=await async function(o){const t=[];try{const n=await e.readdir(o,{recursive:!0});for(const e of n)"string"==typeof e&&(e.endsWith(".js")||e.endsWith(".css")||e.endsWith(".json"))&&t.push(e)}catch(e){console.warn("Warning: Could not collect build assets:",e)}return t}(V.outDir);if(H.end(),y){console.log("š Processing plugin postBuild hooks...");const e=v("Post-build hooks");e.start(),await y.execPostBuildHooks(m,V.outDir,F,Y),e.end(),console.log(" Post-build hooks completed\n")}console.log("š¦ Copying Marko tags directory...");const q=v("Copy tags directory");q.start(),await copyTagsDirectory(h,V.outDir,m,i),q.end(),console.log(" Tags directory copied\n"),console.log("\nā
Build completed successfully!"),console.log(" Output: "+V.outDir),console.log(" Pages: Generated dynamically at request time"),console.log("\nā±ļø Build timing:");const z=Array.from(k.entries()).sort((e,o)=>o[1]-e[1]);for(const[e,o]of z){const t=(o/1e3).toFixed(2);console.log(` ${"ā".repeat(Math.min(Math.floor(o/100),20))} ${e}: ${t}s`)}return{success:!0,outDir:V.outDir,pages:0,errors:w}}catch(e){const o=e instanceof Error?e.message:e+"";return w.push(o),console.error("\nā Build failed:",o),{success:!1,outDir:"",pages:0,errors:w}}}function b(e,o){Array.isArray(e.sidebar)&&(e.sidebar=e.sidebar.map(e=>({...e,items:Array.isArray(e.items)?e.items.map(e=>({...e,link:e.link&&!e.link.startsWith(o)?o+e.link:e.link})):e.items}))),Array.isArray(e.blogPosts)&&(e.blogPosts=e.blogPosts.map(e=>({...e,link:e.link&&!e.link.startsWith(o)?o+e.link:e.link})))}export async function generateRoutes(e,t,n,s,a){await cleanupGeneratedRoutes(t,e,a);const r=[];let i=0,c=0,l=0;for(const[o,t]of Object.entries(e)){if(!Array.isArray(t))continue;r.push(o);const e=t;if("pages"===o)for(const o of e)await v(0,0,0,0,a),i++;else if("blog"===o)for(const o of e)await P(0,0,0,0,a),l++;else for(const o of e)await $(0,0,0,0,a),c++}await C(t,n,a);const d=o.resolve(t,"..","..");await generateViteConfig(d,a),await F(t,n,a),a&&(console.log(` Generated ${i} page routes`),console.log(` Generated ${c} doc routes`),console.log(` Generated ${l} blog routes`))}export async function cleanupGeneratedRoutes(t,n,s){const a=[],r=["+layout.marko","+middleware.js","components/**/*","api/**/*","lib/**/*"],i=Object.keys(n).filter(e=>"pages"!==e).map(e=>e+"/");try{const n=await e.readdir(t,{recursive:!0,withFileTypes:!0});for(const c of n){if(!c.isFile())continue;const n=o.join(c.path||c.parentPath||t,c.name),l=o.relative(t,n);if(i.some(e=>l.startsWith(e)))if(r.some(e=>e.includes("**")?RegExp(e.replace(/\*\*/g,".*").replace(/\*/g,"[^/]*")).test(l):c.name===e))s&&console.log(" Preserving: "+l);else try{await e.unlink(n),s&&console.log(" Deleted: "+l)}catch(e){if("ENOENT"!==e.code){const o=e instanceof Error?e.message:e+"";a.push(`Failed to delete ${l}: ${o}`)}}}for(const e of i){const n=o.join(t,e);try{await T(n)}catch{}}a.length>0&&(console.warn("ā ļø Cleanup warnings:"),a.forEach(e=>console.warn(" "+e)))}catch(e){if("ENOENT"!==e.code)throw e}}async function T(t){try{const n=await e.readdir(t,{withFileTypes:!0});for(const e of n)if(e.isDirectory()){const n=o.join(t,e.name);await T(n)}0===(await e.readdir(t)).length&&await e.rmdir(t)}catch{}}async function v(e,o,t,n,s){s&&console.log(" Warning: Static routes deprecated, use catch-all routes")}async function $(e,o,t,n,s){s&&console.log(" Warning: Static routes deprecated, use catch-all routes")}async function P(e,o,t,n,s){s&&console.log(" Warning: Static routes deprecated, use catch-all routes")}async function C(t,n,s){const a=o.join(t,"_config.js"),r={root:n.root,contentDir:n.contentDir,site:{title:n.site?.title||"MarkoPress",description:n.site?.description||"",lang:n.site?.lang||"en-US",head:n.site?.head||[],base:n.site?.base||"/"},content:n.content,theme:{name:n.theme?.name||"@markopress/theme-default",options:n.theme?.options||{}},markdown:n.markdown||{},build:n.build||{}},i=(n.site?.base||"/").replace(/\/$/,"");i&&r.theme.options?.navbar&&(r.theme.options.navbar=r.theme.options.navbar.map(e=>({...e,link:e.link&&!e.link.startsWith(i)?i+e.link:e.link})));const c=`// Auto-generated by MarkoPress - Do not edit\nexport const config = ${JSON.stringify(r,null,2)};\n`;await e.writeFile(a,c),s&&console.log(" Generated: "+a)}async function F(t,n,s){const a=o.join(t,"+layout.marko"),r=(n.theme,n.site?.title||"MarkoPress"),i=n.theme?.options?.style||"default",c=n.site?.base?.replace(/\/$/,"")||"",l=n.markdown?.markoTags?.enabled?`<link rel="stylesheet" href="${c}/markopress-components.css">`:"",d=await S("layout.marko.template",{SITE_TITLE:r,THEME_STYLE:i,BASE_PATH:c,COMPONENT_STYLES_LINK:l});await e.writeFile(a,d),s&&console.log(" Generated: "+a)}async function S(t,n){const s=o.dirname(new URL(import.meta.url).pathname),a=o.join(s,"..",".."),r=o.join(a,"templates",t);let i=await e.readFile(r,"utf-8");i="true"===n.IS_BUILD?i.replace(/\/\/ \{\{IF_DEV_START\}\}[\s\S]*?\/\/ \{\{IF_DEV_END\}\}/g,""):i.replace(/\/\/ \{\{IF_BUILD_START\}\}[\s\S]*?\/\/ \{\{IF_BUILD_END\}\}/g,""),i=i.replace(/\/\/ \{\{IF_(?:BUILD|DEV)_(?:START|END)\}\}\n?/g,"");for(const[e,o]of Object.entries(n))i=i.replace(RegExp(`\\{\\{${e}\\}\\}`,"g"),o);return i}export function validateThemeName(e){if(!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(e))throw Error(`Invalid theme name: "${e}". Must be a valid npm package name (e.g., "my-theme" or "@org/my-theme")`);if(e.includes(".."))throw Error(`Theme name cannot contain traversal sequences (..): "${e}"`);if(e.includes("\\"))throw Error(`Theme name cannot contain backslashes: "${e}"`);if((e.match(/\//g)||[]).length>1)throw Error(`Theme name can only contain one forward slash (for scoped packages): "${e}"`);if(o.isAbsolute(e))throw Error(`Theme name cannot be an absolute path: "${e}"`)}export async function generateViteConfig(t,n){const s=o.join(t,"vite.config.js");try{if((await e.readFile(s,"utf-8")).includes("markdownContentPlugin"))return void(n&&console.log(" Vite config already has markdownContentPlugin"))}catch{}await e.writeFile(s,"import { defineConfig } from 'vite';\nimport marko from '@marko/run/vite';\nimport { markdownContentPlugin } from 'markopress/build';\n\nexport default defineConfig({\n plugins: [\n marko(),\n markdownContentPlugin(),\n ],\n resolve: {\n // Preserve symlinks for pnpm workspace compatibility\n // This allows Marko to properly discover tags from symlinked packages\n preserveSymlinks: true,\n },\n build: {\n outDir: 'dist',\n },\n});\n"),n&&console.log(" Created vite.config.js with markdownContentPlugin")}export async function copyThemeCSS(t,n,s){const a=o.join(t,"public","_markopress","theme");await e.mkdir(a,{recursive:!0});const r=n.theme?.name||"@markopress/theme-default";try{validateThemeName(r)}catch(e){const o=e instanceof Error?e.message:e+"";throw Error("Security: "+o)}const i=n.theme?.options?.style||"default",c=`theme-${i}.css`,l=[...j(r)?[o.join(k,"public",c)]:[],o.join(t,"..","node_modules",r,"public",c),o.join(t,"node_modules",r,"public",c)];let d=null,u=null;for(const o of l)try{await e.access(o),d=await e.readFile(o,"utf-8"),u=o;break}catch{}if(!d){console.warn(` Warning: Could not find ${c}, using minimal fallback`);const t=`/* Minimal fallback CSS for style: ${i} */\nbody { font-family: system-ui, sans-serif; margin: 0; padding: 0; }`,n=o.join(a,c);return void await e.writeFile(n,t)}const m=o.join(a,c);await e.writeFile(m,d),s&&(console.log(` Copied ${c} from: ${u}`),console.log(" Output: "+m));const g="styles.css",f=[...j(r)?[o.join(k,g)]:[],o.join(t,"..","node_modules",r,"src",g),o.join(t,"node_modules",r,"src",g)];for(const t of f)try{await e.access(t);const n=await e.readFile(t,"utf-8"),r=o.join(a,g);await e.writeFile(r,n),s&&(console.log(` Copied ${g} from: ${t}`),console.log(" Output: "+r));break}catch{}}export async function extractStylesFromMarkoTags(t,n,s){const a=n.markdown?.markoTags?.tagsDir||"src/tags",r=o.join(t,a),i=o.join(t,"public");await e.mkdir(i,{recursive:!0});const c=o.join(i,"markopress-components.css");if(!n.markdown?.markoTags?.enabled){s&&console.log(" Marko tags not enabled, skipping style extraction");try{await e.unlink(c)}catch{}return}try{await e.access(r)}catch{return void(s&&console.log(" No tags directory found at: "+r))}const l=[];if(await async function t(n){const s=await e.readdir(n,{withFileTypes:!0});for(const e of s){const s=o.join(n,e.name);e.isDirectory()?await t(s):e.isFile()&&e.name.endsWith(".marko")&&l.push(s)}}(r),0===l.length)return void(s&&console.log(" No .marko files found in: "+r));const d=[];d.push("/* Custom markdown tag styles"),d.push(" * Loaded globally because request-time virtual markdown modules"),d.push(" * do not emit tag-local CSS assets reliably. */"),d.push("");for(const t of l){const n=o.relative(r,t),s=""===o.dirname(n)?o.basename(n,".marko"):o.join(o.dirname(n),o.basename(n,".marko"));try{const o=await e.readFile(t,"utf-8"),n=/<style\b[^>]*>([\s\S]*?)<\/style>/gi,a=Array.from(o.matchAll(n));if(a.length>0){d.push(`/* ${s}.marko */`);for(const e of a){const o=e[1]||"";if(o){const e=o.split("\n");let t=0;for(;t<e.length&&""===e[t].trim();)t++;let n=e.length-1;for(;n>=t&&""===e[n].trim();)n--;for(let o=t;o<=n;o++){const t=e[o];if(""===t.trim()){d.push("");continue}const n=t.match(/^(\s*)/),s=n?n[1].length:0,a=" ".repeat(Math.floor(s/2)),r=t.trim().replace(/:global\(([^)]+)\)/g,"$1");d.push(a+r)}}}d.push("")}}catch(e){console.warn(` Warning: Could not read file ${t}:`,e)}}const u=d.join("\n");await e.writeFile(c,u),s&&(console.log(` Extracted styles from ${l.length} Marko component(s)`),console.log(" Output: "+c))}async function x(t){const n=await e.readdir(t,{withFileTypes:!0}),s=await Promise.all(n.map(e=>{const n=o.resolve(t,e.name);return e.isDirectory()?x(n):n}));return Array.prototype.concat(...s).filter(e=>e.endsWith(".marko"))}export async function copyThemeComponents(t,n,s){const a=n.theme?.name||"@markopress/theme-default",r=o.join(t,"src"),i=o.join(r,"tags");await e.mkdir(i,{recursive:!0});const c=[...j(a)?[o.join(k,"tags")]:[],o.join(t,"..","node_modules",a,"dist","tags"),o.join(t,"node_modules",a,"dist","tags"),o.join(t,"..","node_modules",a,"src","components"),o.join(t,"node_modules",a,"src","components")];let l=null;for(const o of c)try{await e.access(o),l=o;break}catch{}if(!l)return void(s&&console.warn(" Warning: Could not find theme components, skipping"));const d=await x(l);let u=0;for(const t of d){const n=o.relative(l,t),a=o.join(i,n);let r=!1;try{await e.access(a),r=!0}catch{}r?s&&console.log(" Skipped component (user override exists): "+n):(await e.mkdir(o.dirname(a),{recursive:!0}),await e.copyFile(t,a),u++)}s&&(console.log(` Copied ${u} theme components from: ${l}`),console.log(" Output: "+i))}export async function copyTagsDirectory(t,n,s,a){const r=s.markdown?.markoTags?.tagsDir||"src/tags",i=o.join(t,r),c=o.join(n,"tags");try{await e.access(i)}catch{return void(a&&console.log(" No tags directory found at: "+i))}await e.mkdir(c,{recursive:!0});const l=await e.readdir(i,{withFileTypes:!0});let d=0;for(const t of l){const n=o.join(i,t.name),s=o.join(c,t.name);if(t.isDirectory()){await e.mkdir(s,{recursive:!0});const t=await e.readdir(n,{withFileTypes:!0});for(const a of t){const t=o.join(n,a.name),r=o.join(s,a.name);a.isDirectory()||(await e.copyFile(t,r),d++)}}else t.isFile()&&(await e.copyFile(n,s),d++)}a&&(console.log(` Copied ${d} tag files from: ${i}`),console.log(" Output: "+c))}export async function generateCatchAllRoutes(t,n,s,a,r,i=!0){console.log(" Using catch-all dynamic routes..."),console.log(" Mode: "+(i?"build (pre-compiled)":"dev (request-time rendering)"));const c=Object.keys(s.content||{}),l=new Set(a.map(e=>e.id)),d=[...new Set([...c,...l])];if(a.some(e=>"root"===e.id)){const t=o.join(n,"$$slug");await e.mkdir(t,{recursive:!0});const s=await S("catch-all-handler.js.template",{CONTENT_TYPE:"root",CONFIG_PATH:"../_config.js",VITE_PLUGIN_PATH:"markopress/build",IS_BUILD:i?"true":"false"});await e.writeFile(o.join(t,"+handler.js"),s);const a=await S("catch-all-page.marko.template",{CONTENT_TYPE_CLASS:"page"});await e.writeFile(o.join(t,"+page.marko"),a),r&&console.log(" Generated root pages catch-all route")}for(const t of d){if("root"===t)continue;const s=o.join(n,t,"$$slug");await e.mkdir(s,{recursive:!0});const a=await S("catch-all-handler.js.template",{CONTENT_TYPE:t,CONFIG_PATH:"../../_config.js",VITE_PLUGIN_PATH:"markopress/build",IS_BUILD:i?"true":"false"});await e.writeFile(o.join(s,"+handler.js"),a);const c=await S("catch-all-page.marko.template",{CONTENT_TYPE_CLASS:t});await e.writeFile(o.join(s,"+page.marko"),c),r&&console.log(` Generated ${t} catch-all route`)}await C(n,s,r);const u=o.resolve(n,"..","..");await generateViteConfig(u,r),await F(n,s,r)}export{u as loadMarkdownModule,m as registerMarkdownContent};export{markdownContentPlugin}from"./vite-markdown-plugin.js";
|
|
1
|
+
import{promises as e}from"node:fs";import o from"node:path";import{spawn as t}from"node:child_process";import{fileURLToPath as n}from"node:url";import s from"gray-matter";import{loadConfig as a}from"../config/loader.js";import{getDesignSystem as r,getDarkModeOverride as i}from"../theme/default/design-systems/index.js";import{globalTagValidator as c,formatValidationError as l}from"../markdown/index.js";import{PluginManager as d}from"../plugin/manager.js";import{loadMarkdownModule as u,registerMarkdownContent as m,escapeMarkoText as g}from"./vite-markdown-plugin.js";import{renderMarkdown as f}from"../markdown/renderer.js";import{buildSearchIndex as p}from"../search/index.js";const h=o.dirname(n(import.meta.url)),w=o.resolve(h,"..",".."),k=o.join(w,"src","theme","default"),y=new Set(["@markopress/theme-default","theme-default","default"]);function j(e){return y.has(e)}export function filePathToUrl(e,t){const n=o.relative(t,e).replace(/\.md$/,"").split(o.sep).join("/");return"index"===n?"/":n.endsWith("/index")?"/"+n.replace("/index",""):"/"+n}export async function build(n={}){const{outDir:r,debug:i=!1,useCatchAllRoutes:u,root:m}=n,h=m||process.cwd(),w=[],k=new Map,y=new Map,j=o.join(h,".markopress");let T=h;try{(await e.stat(j)).isDirectory()&&(T=j)}catch{}const v=e=>({start:()=>{y.set(e,performance.now())},end:()=>{const o=y.get(e)||0,t=performance.now()-o;k.set(e,t)}});try{console.log("š Building MarkoPress site...\n");const n=v("Config loading");n.start();let m,y=await a(h,{mode:"production",command:"build"});if(n.end(),y.plugins&&y.plugins.length>0){console.log("š Loading plugins...");const e=v("Plugin loading");e.start(),m=new d(y),await m.loadPlugins(y.plugins),y=m.getConfig(),e.end(),console.log("")}if(m){console.log("š¦ Loading plugin content...");const e=v("Plugin loadContent hooks");e.start(),await m.execLoadContentHooks(),e.end(),console.log(" Plugin content loaded\n")}const j={},$=[],C=o.resolve(h,y.contentDir);try{const t=await e.readdir(C,{withFileTypes:!0,recursive:!0}),n=new Map;for(const a of t){if(!a.isFile()||!a.name.endsWith(".md"))continue;const t=o.join(a.path||a.parentPath||C,a.name),r=o.relative(C,t),i=filePathToUrl(t,C),c=r.split(o.sep),l=1===c.length?"root":c[0],d=await e.readFile(t,"utf-8");let u={};try{u=s(d).data}catch{}n.has(l)||n.set(l,[]),n.get(l).push({id:a.name.replace(".md",""),slug:a.name.replace(".md",""),filePath:t,urlPath:i,directory:l,processed:{frontmatter:u}})}for(const[e,t]of n){const n=y.content[e]||{},s=new Map;$.push({id:e,dir:o.join(C,"root"===e?"":e),config:n,features:n,files:t,enhance(e,o){s.set(e,o)},getEnhancement:e=>s.get(e),_enhancements:s})}}catch(e){console.warn("Warning: Could not scan content directory: "+e)}if(m&&$.length>0){console.log("š Enhancing modules with plugin metadata...");const t=v("Module enhancement");t.start();for(const e of $)console.log(` Module: ${e.id} (${e.files.length} files)`),e.files.length>0&&console.log(" First file has processed: "+!!e.files[0].processed);await m.execEnhanceModulesHooks($),t.end(),console.log(` Enhanced ${$.length} module(s)\n`);const n=o.join(T,"src",".generated");await e.mkdir(n,{recursive:!0});const s=(y.site?.base||"/").replace(/\/$/,""),a={};for(const e of $){const o={},t=e._enhancements.entries();for(const[e,n]of t)o[e]=n;Object.keys(o).length>0&&(s&&b(o,s),a[e.id]=o)}const r=o.join(n,"module-enhancements.js"),i=`// Auto-generated by MarkoPress - Do not edit\nexport default ${JSON.stringify(a,null,2)};\n`;await e.writeFile(r,i,"utf-8"),console.log(" Wrote module enhancements to src/.generated/module-enhancements.js\n")}if(!1!==y.search?.enabled){console.log("š Building search index...");const t=v("Search index");t.start();const n=[],s=(y.site?.base||"/").replace(/\/$/,"");for(const o of $)for(const t of o.files)try{const o=await e.readFile(t.filePath,"utf-8"),a=await f(o,{...y.markdown,base:s});n.push({url:t.urlPath,html:a.html,title:a.frontmatter?.title||t.id,frontmatter:a.frontmatter})}catch(e){console.warn(` Warning: Could not index ${t.filePath}:`,e)}try{const t=await p(n,y.search),s=o.join(T,"public","search-index.json");await e.mkdir(o.dirname(s),{recursive:!0}),await e.writeFile(s,t),console.log(` Search index built (${n.length} pages)\n`)}catch(e){console.warn(" Warning: Failed to build search index:",e)}t.end()}if(y.markdown.markoTags?.enabled){const e=o.join(T,y.markdown.markoTags.tagsDir||"src/tags");console.log("š Scanning tags directory...");const t=v("Tag validation setup");t.start(),await c.loadAvailableTags(e),t.end(),console.log(` Found ${c.getAvailableTagsCount()} tags\n`)}else c.reset();const P=o.join(T,"src","routes");await e.mkdir(P,{recursive:!0});let F={};if(m){const e=v("Extend routes hooks");e.start(),F=await m.execExtendRoutesHooks(F),e.end(),console.log("š Extended routes manifest:",Object.keys(F).length)}console.log("š Generating routes from content...");const S=v("Route generation");S.start();const x=u??y.build.useCatchAllRoutes;console.log("š Pre-rendering markdown to .marko files...");const E=v("Pre-render markdown");E.start();const _=o.join(T,"src",".generated","markdown");await e.mkdir(_,{recursive:!0});const D={};let A=0;for(const t of $){const n=o.join(_,t.id);await e.mkdir(n,{recursive:!0});for(const s of t.files)try{const a=await e.readFile(s.filePath,"utf-8"),r=(y.site?.base||"/").replace(/\/$/,""),i=await f(a,{base:r});let c=i.html.replace(/<span class="line"><\/span>(\s*<\/code>)/g,"$1");const l=`<div class="markdown-content">\n${g(c)}\n</div>`,d=o.join(n,s.slug+".marko");await e.writeFile(d,l),D[`${t.id}/${s.slug}`]={frontmatter:i.frontmatter,headers:i.headers||[],headTop:s.headTop||[],headBottom:s.headBottom||[]},A++}catch(e){console.warn(` Warning: Failed to pre-render ${t.id}/${s.slug}:`,e)}}const N=o.join(T,"src",".generated","content-metadata.js"),M=`// Auto-generated by MarkoPress - Do not edit\nexport default ${JSON.stringify(D,null,2)};\n`;await e.writeFile(N,M),E.end(),console.log(` Pre-rendered ${A} markdown files\n`),x?(await generateCatchAllRoutes(j,P,y,$,i,!0),console.log(" Using catch-all dynamic routes")):(await generateRoutes(j,P,y,$,i),console.log(" Using static routes")),S.end(),console.log(" Routes generated\n");const O=[];for(const[e,o]of Object.entries(F))(o.handler||o.component)&&(O.push({path:e,...o}),console.log(" Found plugin route: "+e));if(console.log(`š Total manifest routes: ${Object.keys(F).length}, Plugin routes: ${O.length}`),m){const t=[...m.getPluginRoutes(),...O];if(t.length>0){console.log(`š Generating ${t.length} plugin routes...`);const n=v("Plugin route generation");n.start(),await async function(t,n,s,a){for(const s of t){const t=s.path.slice(1),r=o.join(n,t,"+page");if(await e.mkdir(o.dirname(r),{recursive:!0}),s.handler){const t=o.join(o.dirname(r),"+handler.js");await e.writeFile(t,s.handler)}if(s.component){const o=r+".marko";await e.writeFile(o,s.component)}a&&console.log(" Generated plugin route: "+s.path)}}(t,P,0,i),n.end(),console.log(" Plugin routes generated\n")}}console.log("āļø Generating Vite config...");const I=v("Vite config generation");if(I.start(),await generateViteConfig(T,i),I.end(),console.log(" Vite config generated\n"),m){console.log("š Processing plugin allContentLoaded hooks...");const e=v("AllContentLoaded hooks");e.start(),await m.execAllContentLoadedHooks(F),e.end(),console.log(" All content processed\n")}if(y.markdown.markoTags?.enabled){console.log("š Validating Marko tags...");const e=v("Tag validation");e.start();const o=c.validate();if(e.end(),!o.success){const e=l(o.missingTags);return console.error(`\n${e}\n`),w.push(e),{success:!1,outDir:"",pages:0,errors:w}}console.log(" All tags validated ā\n")}console.log("šØ Copying theme CSS...");const L=v("Theme CSS copy");L.start(),await copyThemeCSS(T,y,i),L.end(),console.log(" Theme CSS copied\n"),console.log("šØ Extracting styles from Marko components...");const W=v("Marko component styles extraction");W.start(),await extractStylesFromMarkoTags(T,y,i),W.end(),console.log(" Component styles extracted\n");const R=[];for(const e of $)for(const o of e.files)R.push(o.urlPath);for(const e of Object.keys(F))R.includes(e)||R.push(e);const G=o.join(T,"src",".generated","static-urls.json");await e.mkdir(o.dirname(G),{recursive:!0}),await e.writeFile(G,JSON.stringify(R,null,2)),i&&console.log(` Generated static URL manifest: ${R.length} URLs`),console.log("šØ Building with @marko/run...");const B=v("@marko/run build");B.start();const U=r||y.build.outDir,V=await async function(e,n,s){return new Promise(a=>{const r=["build"];e&&r.push("--output",e),n&&r.push("--debug");const i=t("npx",["marko-run",...r],{stdio:"inherit",cwd:s});i.on("close",t=>{if(0===t){const t=e||"dist";a({success:!0,outDir:o.join(s,t),errors:[]})}else a({success:!1,outDir:"",errors:["Build process exited with code "+t]})}),i.on("error",e=>{a({success:!1,outDir:"",errors:["Failed to start build process: "+e.message]})})})}(U,i,T);if(B.end(),!V.success)return w.push(...V.errors),{success:!1,outDir:"",pages:0,errors:w};const H=v("Collect build assets");H.start();const Y=await async function(o){const t=[];try{const n=await e.readdir(o,{recursive:!0});for(const e of n)"string"==typeof e&&(e.endsWith(".js")||e.endsWith(".css")||e.endsWith(".json"))&&t.push(e)}catch(e){console.warn("Warning: Could not collect build assets:",e)}return t}(V.outDir);if(H.end(),m){console.log("š Processing plugin postBuild hooks...");const e=v("Post-build hooks");e.start(),await m.execPostBuildHooks(y,V.outDir,F,Y),e.end(),console.log(" Post-build hooks completed\n")}console.log("š¦ Copying Marko tags directory...");const q=v("Copy tags directory");q.start(),await copyTagsDirectory(h,V.outDir,y,i),q.end(),console.log(" Tags directory copied\n"),console.log("\nā
Build completed successfully!"),console.log(" Output: "+V.outDir),console.log(" Pages: Generated dynamically at request time"),console.log("\nā±ļø Build timing:");const z=Array.from(k.entries()).sort((e,o)=>o[1]-e[1]);for(const[e,o]of z){const t=(o/1e3).toFixed(2);console.log(` ${"ā".repeat(Math.min(Math.floor(o/100),20))} ${e}: ${t}s`)}return{success:!0,outDir:V.outDir,pages:0,errors:w}}catch(e){const o=e instanceof Error?e.message:e+"";return w.push(o),console.error("\nā Build failed:",o),{success:!1,outDir:"",pages:0,errors:w}}}function b(e,o){Array.isArray(e.sidebar)&&(e.sidebar=e.sidebar.map(e=>({...e,items:Array.isArray(e.items)?e.items.map(e=>({...e,link:e.link&&!e.link.startsWith(o)?o+e.link:e.link})):e.items}))),Array.isArray(e.blogPosts)&&(e.blogPosts=e.blogPosts.map(e=>({...e,link:e.link&&!e.link.startsWith(o)?o+e.link:e.link})))}export async function generateRoutes(e,t,n,s,a){await cleanupGeneratedRoutes(t,e,a);const r=[];let i=0,c=0,l=0;for(const[o,t]of Object.entries(e)){if(!Array.isArray(t))continue;r.push(o);const e=t;if("pages"===o)for(const o of e)await v(0,0,0,0,a),i++;else if("blog"===o)for(const o of e)await C(0,0,0,0,a),l++;else for(const o of e)await $(0,0,0,0,a),c++}await P(t,n,a);const d=o.resolve(t,"..","..");await generateViteConfig(d,a),await F(t,n,a),a&&(console.log(` Generated ${i} page routes`),console.log(` Generated ${c} doc routes`),console.log(` Generated ${l} blog routes`))}export async function cleanupGeneratedRoutes(t,n,s){const a=[],r=["+layout.marko","+middleware.js","components/**/*","api/**/*","lib/**/*"],i=Object.keys(n).filter(e=>"pages"!==e).map(e=>e+"/");try{const n=await e.readdir(t,{recursive:!0,withFileTypes:!0});for(const c of n){if(!c.isFile())continue;const n=o.join(c.path||c.parentPath||t,c.name),l=o.relative(t,n);if(i.some(e=>l.startsWith(e)))if(r.some(e=>e.includes("**")?RegExp(e.replace(/\*\*/g,".*").replace(/\*/g,"[^/]*")).test(l):c.name===e))s&&console.log(" Preserving: "+l);else try{await e.unlink(n),s&&console.log(" Deleted: "+l)}catch(e){if("ENOENT"!==e.code){const o=e instanceof Error?e.message:e+"";a.push(`Failed to delete ${l}: ${o}`)}}}for(const e of i){const n=o.join(t,e);try{await T(n)}catch{}}a.length>0&&(console.warn("ā ļø Cleanup warnings:"),a.forEach(e=>console.warn(" "+e)))}catch(e){if("ENOENT"!==e.code)throw e}}async function T(t){try{const n=await e.readdir(t,{withFileTypes:!0});for(const e of n)if(e.isDirectory()){const n=o.join(t,e.name);await T(n)}0===(await e.readdir(t)).length&&await e.rmdir(t)}catch{}}async function v(e,o,t,n,s){s&&console.log(" Warning: Static routes deprecated, use catch-all routes")}async function $(e,o,t,n,s){s&&console.log(" Warning: Static routes deprecated, use catch-all routes")}async function C(e,o,t,n,s){s&&console.log(" Warning: Static routes deprecated, use catch-all routes")}async function P(t,n,s){const a=o.join(t,"_config.js"),r={root:n.root,contentDir:n.contentDir,site:{title:n.site?.title||"MarkoPress",description:n.site?.description||"",lang:n.site?.lang||"en-US",head:n.site?.head||[],base:n.site?.base||"/"},content:n.content,theme:{name:n.theme?.name||"@markopress/theme-default",options:n.theme?.options||{}},markdown:n.markdown||{},build:n.build||{},_headInject:n._headInject||void 0},i=(n.site?.base||"/").replace(/\/$/,"");i&&r.theme.options?.navbar&&(r.theme.options.navbar=r.theme.options.navbar.map(e=>({...e,link:e.link&&!e.link.startsWith(i)?i+e.link:e.link})));const c=`// Auto-generated by MarkoPress - Do not edit\nexport const config = ${JSON.stringify(r,null,2)};\n`;await e.writeFile(a,c),s&&console.log(" Generated: "+a)}async function F(t,n,s){const a=o.join(t,"+layout.marko"),r=(n.theme,n.site?.title||"MarkoPress"),i=n.theme?.options?.style||"default",c=n.site?.base?.replace(/\/$/,"")||"",l=n.markdown?.markoTags?.enabled?`<link rel="stylesheet" href="${c}/markopress-components.css">`:"",d=await S("layout.marko.template",{SITE_TITLE:r,THEME_STYLE:i,BASE_PATH:c,COMPONENT_STYLES_LINK:l});await e.writeFile(a,d),s&&console.log(" Generated: "+a)}async function S(t,n){const s=o.dirname(new URL(import.meta.url).pathname),a=o.join(s,"..",".."),r=o.join(a,"templates",t);let i=await e.readFile(r,"utf-8");i="true"===n.IS_BUILD?i.replace(/\/\/ \{\{IF_DEV_START\}\}[\s\S]*?\/\/ \{\{IF_DEV_END\}\}/g,""):i.replace(/\/\/ \{\{IF_BUILD_START\}\}[\s\S]*?\/\/ \{\{IF_BUILD_END\}\}/g,""),i=i.replace(/\/\/ \{\{IF_(?:BUILD|DEV)_(?:START|END)\}\}\n?/g,"");for(const[e,o]of Object.entries(n))i=i.replace(RegExp(`\\{\\{${e}\\}\\}`,"g"),o);return i}export function validateThemeName(e){if(!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(e))throw Error(`Invalid theme name: "${e}". Must be a valid npm package name (e.g., "my-theme" or "@org/my-theme")`);if(e.includes(".."))throw Error(`Theme name cannot contain traversal sequences (..): "${e}"`);if(e.includes("\\"))throw Error(`Theme name cannot contain backslashes: "${e}"`);if((e.match(/\//g)||[]).length>1)throw Error(`Theme name can only contain one forward slash (for scoped packages): "${e}"`);if(o.isAbsolute(e))throw Error(`Theme name cannot be an absolute path: "${e}"`)}export async function generateViteConfig(t,n){const s=o.join(t,"vite.config.js");try{if((await e.readFile(s,"utf-8")).includes("markdownContentPlugin"))return void(n&&console.log(" Vite config already has markdownContentPlugin"))}catch{}await e.writeFile(s,"import { defineConfig } from 'vite';\nimport marko from '@marko/run/vite';\nimport { markdownContentPlugin } from 'markopress/build';\n\nexport default defineConfig({\n plugins: [\n marko(),\n markdownContentPlugin(),\n ],\n resolve: {\n // Preserve symlinks for pnpm workspace compatibility\n // This allows Marko to properly discover tags from symlinked packages\n preserveSymlinks: true,\n },\n build: {\n outDir: 'dist',\n },\n});\n"),n&&console.log(" Created vite.config.js with markdownContentPlugin")}export async function copyThemeCSS(t,n,s){const a=o.join(t,"public","_markopress","theme");await e.mkdir(a,{recursive:!0});const r=n.theme?.name||"@markopress/theme-default";try{validateThemeName(r)}catch(e){const o=e instanceof Error?e.message:e+"";throw Error("Security: "+o)}const i=n.theme?.options?.style||"default",c=`theme-${i}.css`,l=[...j(r)?[o.join(k,"public",c)]:[],o.join(t,"..","node_modules",r,"public",c),o.join(t,"node_modules",r,"public",c)];let d=null,u=null;for(const o of l)try{await e.access(o),d=await e.readFile(o,"utf-8"),u=o;break}catch{}if(!d){console.warn(` Warning: Could not find ${c}, using minimal fallback`);const t=`/* Minimal fallback CSS for style: ${i} */\nbody { font-family: system-ui, sans-serif; margin: 0; padding: 0; }`,n=o.join(a,c);return void await e.writeFile(n,t)}const m=o.join(a,c);await e.writeFile(m,d),s&&(console.log(` Copied ${c} from: ${u}`),console.log(" Output: "+m));const g="styles.css",f=[...j(r)?[o.join(k,g)]:[],o.join(t,"..","node_modules",r,"src",g),o.join(t,"node_modules",r,"src",g)];for(const t of f)try{await e.access(t);const n=await e.readFile(t,"utf-8"),r=o.join(a,g);await e.writeFile(r,n),s&&(console.log(` Copied ${g} from: ${t}`),console.log(" Output: "+r));break}catch{}}export async function extractStylesFromMarkoTags(t,n,s){const a=n.markdown?.markoTags?.tagsDir||"src/tags",r=o.join(t,a),i=o.join(t,"public");await e.mkdir(i,{recursive:!0});const c=o.join(i,"markopress-components.css");if(!n.markdown?.markoTags?.enabled){s&&console.log(" Marko tags not enabled, skipping style extraction");try{await e.unlink(c)}catch{}return}try{await e.access(r)}catch{return void(s&&console.log(" No tags directory found at: "+r))}const l=[];if(await async function t(n){const s=await e.readdir(n,{withFileTypes:!0});for(const e of s){const s=o.join(n,e.name);e.isDirectory()?await t(s):e.isFile()&&e.name.endsWith(".marko")&&l.push(s)}}(r),0===l.length)return void(s&&console.log(" No .marko files found in: "+r));const d=[];d.push("/* Custom markdown tag styles"),d.push(" * Loaded globally because request-time virtual markdown modules"),d.push(" * do not emit tag-local CSS assets reliably. */"),d.push("");for(const t of l){const n=o.relative(r,t),s=""===o.dirname(n)?o.basename(n,".marko"):o.join(o.dirname(n),o.basename(n,".marko"));try{const o=await e.readFile(t,"utf-8"),n=/<style\b[^>]*>([\s\S]*?)<\/style>/gi,a=Array.from(o.matchAll(n));if(a.length>0){d.push(`/* ${s}.marko */`);for(const e of a){const o=e[1]||"";if(o){const e=o.split("\n");let t=0;for(;t<e.length&&""===e[t].trim();)t++;let n=e.length-1;for(;n>=t&&""===e[n].trim();)n--;for(let o=t;o<=n;o++){const t=e[o];if(""===t.trim()){d.push("");continue}const n=t.match(/^(\s*)/),s=n?n[1].length:0,a=" ".repeat(Math.floor(s/2)),r=t.trim().replace(/:global\(([^)]+)\)/g,"$1");d.push(a+r)}}}d.push("")}}catch(e){console.warn(` Warning: Could not read file ${t}:`,e)}}const u=d.join("\n");await e.writeFile(c,u),s&&(console.log(` Extracted styles from ${l.length} Marko component(s)`),console.log(" Output: "+c))}async function x(t){const n=await e.readdir(t,{withFileTypes:!0}),s=await Promise.all(n.map(e=>{const n=o.resolve(t,e.name);return e.isDirectory()?x(n):n}));return Array.prototype.concat(...s).filter(e=>e.endsWith(".marko"))}export async function copyThemeComponents(t,n,s){const a=n.theme?.name||"@markopress/theme-default",r=o.join(t,"src"),i=o.join(r,"tags");await e.mkdir(i,{recursive:!0});const c=[...j(a)?[o.join(k,"tags")]:[],o.join(t,"..","node_modules",a,"dist","tags"),o.join(t,"node_modules",a,"dist","tags"),o.join(t,"..","node_modules",a,"src","components"),o.join(t,"node_modules",a,"src","components")];let l=null;for(const o of c)try{await e.access(o),l=o;break}catch{}if(!l)return void(s&&console.warn(" Warning: Could not find theme components, skipping"));const d=await x(l);let u=0;for(const t of d){const n=o.relative(l,t),a=o.join(i,n);let r=!1;try{await e.access(a),r=!0}catch{}r?s&&console.log(" Skipped component (user override exists): "+n):(await e.mkdir(o.dirname(a),{recursive:!0}),await e.copyFile(t,a),u++)}s&&(console.log(` Copied ${u} theme components from: ${l}`),console.log(" Output: "+i))}export async function copyTagsDirectory(t,n,s,a){const r=s.markdown?.markoTags?.tagsDir||"src/tags",i=o.join(t,r),c=o.join(n,"tags");try{await e.access(i)}catch{return void(a&&console.log(" No tags directory found at: "+i))}await e.mkdir(c,{recursive:!0});const l=await e.readdir(i,{withFileTypes:!0});let d=0;for(const t of l){const n=o.join(i,t.name),s=o.join(c,t.name);if(t.isDirectory()){await e.mkdir(s,{recursive:!0});const t=await e.readdir(n,{withFileTypes:!0});for(const a of t){const t=o.join(n,a.name),r=o.join(s,a.name);a.isDirectory()||(await e.copyFile(t,r),d++)}}else t.isFile()&&(await e.copyFile(n,s),d++)}a&&(console.log(` Copied ${d} tag files from: ${i}`),console.log(" Output: "+c))}export async function generateCatchAllRoutes(t,n,s,a,r,i=!0){console.log(" Using catch-all dynamic routes..."),console.log(" Mode: "+(i?"build (pre-compiled)":"dev (request-time rendering)"));const c=Object.keys(s.content||{}),l=new Set(a.map(e=>e.id)),d=[...new Set([...c,...l])];if(a.some(e=>"root"===e.id)){const t=o.join(n,"$$slug");await e.mkdir(t,{recursive:!0});const s=await S("catch-all-handler.js.template",{CONTENT_TYPE:"root",CONFIG_PATH:"../_config.js",VITE_PLUGIN_PATH:"markopress/build",IS_BUILD:i?"true":"false"});await e.writeFile(o.join(t,"+handler.js"),s);const a=await S("catch-all-page.marko.template",{CONTENT_TYPE_CLASS:"page"});await e.writeFile(o.join(t,"+page.marko"),a),r&&console.log(" Generated root pages catch-all route")}for(const t of d){if("root"===t)continue;const s=o.join(n,t,"$$slug");await e.mkdir(s,{recursive:!0});const a=await S("catch-all-handler.js.template",{CONTENT_TYPE:t,CONFIG_PATH:"../../_config.js",VITE_PLUGIN_PATH:"markopress/build",IS_BUILD:i?"true":"false"});await e.writeFile(o.join(s,"+handler.js"),a);const c=await S("catch-all-page.marko.template",{CONTENT_TYPE_CLASS:t});await e.writeFile(o.join(s,"+page.marko"),c),r&&console.log(` Generated ${t} catch-all route`)}await P(n,s,r);const u=o.resolve(n,"..","..");await generateViteConfig(u,r),await F(n,s,r)}export{u as loadMarkdownModule,m as registerMarkdownContent};export{markdownContentPlugin}from"./vite-markdown-plugin.js";
|
package/dist/config/loader.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import o from"node:path";import e from"node:fs/promises";import{pathToFileURL as t}from"node:url";import{ZodError as n}from"zod";import{validateConfigSafe as r}from"./validation.js";const i={site:{title:"MarkoPress Site",description:"",base:"/",lang:"en-US",head:[]},contentDir:"content",content:{docs:{sidebar:!0},blog:{rss:!0,list:!0}},theme:{name:"@markopress/theme-default",options:{}},markdown:{lineNumbers:!1,theme:{light:"github-light",dark:"github-dark"}},build:{useCatchAllRoutes:!0,outDir:"dist",assetsDir:"assets"},search:{enabled:!0},plugins:[]};function s(o,e){const t={...o};for(const o in e){const n=e[o],r=t[o];void 0!==n&&(a(r)&&a(n)?t[o]=s(r,n):t[o]=n)}return t}function a(o){return null!==o&&"object"==typeof o&&!Array.isArray(o)}function c(o){const e=r(o);return e.success?{valid:!0,errors:[],data:e.data}:{valid:!1,errors:e.errors.map(o=>`${o.path}: ${o.message}`)}}export async function loadConfigFromFile(n,r){const i=["config.ts","config.js","config.mjs",".markopress/config.ts",".markopress/config.js",".markopress/config.mjs"];for(const s of i){const i=o.resolve(n,s);try{await e.access(i);const o=`${t(i).href}?t=${Date.now()}`,n=(await import(o)).default;if(!n)throw Error(`Config file ${s} must have a default export`);let a;a="function"==typeof n?await n(r):n;const f=c(a);if(!f.valid)throw Error(`Invalid configuration in ${s}:\n${f.errors.map(o=>" - "+o).join("\n")}`);return console.log("ā Loaded config from "+s),{file:i,config:f.data}}catch(o){if(o&&"object"==typeof o&&"code"in o&&"ENOENT"!==o.code)throw Error(`Failed to load config from ${s}: ${o instanceof Error?o.message:o+""}`)}}return null}export function resolveConfig(o,e){const t=o.content?{...o.content}:i.content,n=o.build?{...o.build}:i.build,r=s(i.site,o.site||{}),a=s(i.theme,o.theme||{}),c=s(i.markdown,o.markdown||{}),f=s(i.search,o.search||{}),l=o.plugins||i.plugins,d=o.seo,m=a.options?.style||"default",u=
|
|
1
|
+
import o from"node:path";import e from"node:fs/promises";import{pathToFileURL as t}from"node:url";import{ZodError as n}from"zod";import{validateConfigSafe as r}from"./validation.js";const i={site:{title:"MarkoPress Site",description:"",base:"/",lang:"en-US",head:[]},contentDir:"content",content:{docs:{sidebar:!0},blog:{rss:!0,list:!0}},theme:{name:"@markopress/theme-default",options:{}},markdown:{lineNumbers:!1,theme:{light:"github-light",dark:"github-dark"}},build:{useCatchAllRoutes:!0,outDir:"dist",assetsDir:"assets"},search:{enabled:!0},plugins:[]};function s(o,e){const t={...o};for(const o in e){const n=e[o],r=t[o];void 0!==n&&(a(r)&&a(n)?t[o]=s(r,n):t[o]=n)}return t}function a(o){return null!==o&&"object"==typeof o&&!Array.isArray(o)}function c(o){const e=r(o);return e.success?{valid:!0,errors:[],data:e.data}:{valid:!1,errors:e.errors.map(o=>`${o.path}: ${o.message}`)}}export async function loadConfigFromFile(n,r){const i=["config.ts","config.js","config.mjs",".markopress/config.ts",".markopress/config.js",".markopress/config.mjs"];for(const s of i){const i=o.resolve(n,s);try{await e.access(i);const o=`${t(i).href}?t=${Date.now()}`,n=(await import(o)).default;if(!n)throw Error(`Config file ${s} must have a default export`);let a;a="function"==typeof n?await n(r):n;const f=c(a);if(!f.valid)throw Error(`Invalid configuration in ${s}:\n${f.errors.map(o=>" - "+o).join("\n")}`);return console.log("ā Loaded config from "+s),{file:i,config:f.data}}catch(o){if(o&&"object"==typeof o&&"code"in o&&"ENOENT"!==o.code)throw Error(`Failed to load config from ${s}: ${o instanceof Error?o.message:o+""}`)}}return null}export function resolveConfig(o,e){const t=o.content?{...o.content}:i.content,n=o.build?{...o.build}:i.build,r=s(i.site,o.site||{}),a=s(i.theme,o.theme||{}),c=s(i.markdown,o.markdown||{}),f=s(i.search,o.search||{}),l=o.plugins||i.plugins,d=o.seo,m=a.options?.style||"default",u={type:"link",rel:"stylesheet",href:`${(r.base||"/").replace(/\/$/,"")}/_markopress/theme/theme-${m}.css`};return r.head=[...r.head||[],u],{root:e,site:r,contentDir:o.contentDir??i.contentDir,content:t,theme:a,markdown:c,build:n,search:f,plugins:l,seo:d}}export async function loadConfig(o=process.cwd(),e={mode:"development",command:"dev"}){const t=await loadConfigFromFile(o,e);return resolveConfig(t?t.config:{site:i.site},o)}export function defineConfig(o){return o}export function defineConfigWithCallback(o){return o}
|
package/dist/config/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Configuration types for MarkoPress
|
|
3
3
|
*/
|
|
4
4
|
import type { SeoPluginConfig } from '../plugins/seo/types.js';
|
|
5
|
+
import type { HeadTag } from '../plugins/head-inject/types.js';
|
|
5
6
|
export interface SiteConfig {
|
|
6
7
|
title: string;
|
|
7
8
|
description?: string;
|
|
@@ -9,7 +10,11 @@ export interface SiteConfig {
|
|
|
9
10
|
lang?: string;
|
|
10
11
|
head?: HeadTag[];
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Re-export head-inject plugin types for user convenience
|
|
15
|
+
* Use these for type-safe head tag configuration
|
|
16
|
+
*/
|
|
17
|
+
export type { HeadTag } from '../plugins/head-inject/types.js';
|
|
13
18
|
/**
|
|
14
19
|
* Module-specific options
|
|
15
20
|
*
|
|
@@ -171,6 +176,10 @@ export interface ResolvedConfig extends Omit<Required<Omit<MarkoPressConfig, 'co
|
|
|
171
176
|
content: NewContentConfig;
|
|
172
177
|
build: BuildConfig;
|
|
173
178
|
seo?: SeoPluginConfig;
|
|
179
|
+
_headInject?: {
|
|
180
|
+
headTop: Array<[string, Record<string, unknown>]> | Array<[string, Record<string, unknown>, string]>;
|
|
181
|
+
headBottom: Array<[string, Record<string, unknown>]> | Array<[string, Record<string, unknown>, string]>;
|
|
182
|
+
};
|
|
174
183
|
}
|
|
175
184
|
export type ConfigFn = (env: ConfigEnv) => UserConfig | Promise<UserConfig>;
|
|
176
185
|
export interface ConfigEnv {
|
|
@@ -12,7 +12,14 @@ export declare const MarkoPressConfigSchema: z.ZodObject<{
|
|
|
12
12
|
description: z.ZodOptional<z.ZodString>;
|
|
13
13
|
base: z.ZodOptional<z.ZodString>;
|
|
14
14
|
lang: z.ZodOptional<z.ZodString>;
|
|
15
|
-
head: z.ZodOptional<z.ZodArray<z.
|
|
15
|
+
head: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
16
|
+
type: z.ZodEnum<{
|
|
17
|
+
script: "script";
|
|
18
|
+
link: "link";
|
|
19
|
+
meta: "meta";
|
|
20
|
+
base: "base";
|
|
21
|
+
}>;
|
|
22
|
+
}, z.core.$loose>>>;
|
|
16
23
|
}, z.core.$strip>;
|
|
17
24
|
contentDir: z.ZodOptional<z.ZodString>;
|
|
18
25
|
content: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
@@ -88,7 +95,10 @@ export declare function validateConfig(config: unknown): {
|
|
|
88
95
|
description?: string;
|
|
89
96
|
base?: string;
|
|
90
97
|
lang?: string;
|
|
91
|
-
head?:
|
|
98
|
+
head?: {
|
|
99
|
+
[x: string]: unknown;
|
|
100
|
+
type: "script" | "link" | "meta" | "base";
|
|
101
|
+
}[];
|
|
92
102
|
};
|
|
93
103
|
contentDir?: string;
|
|
94
104
|
content?: Record<string, string | {
|
|
@@ -174,7 +184,10 @@ export declare function validateConfigSafe(config: unknown): {
|
|
|
174
184
|
description?: string;
|
|
175
185
|
base?: string;
|
|
176
186
|
lang?: string;
|
|
177
|
-
head?:
|
|
187
|
+
head?: {
|
|
188
|
+
[x: string]: unknown;
|
|
189
|
+
type: "script" | "link" | "meta" | "base";
|
|
190
|
+
}[];
|
|
178
191
|
};
|
|
179
192
|
contentDir?: string;
|
|
180
193
|
content?: Record<string, string | {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{z as o}from"zod";const e=o.object({title:o.string().min(1,{message:"Site title is required"}).max(100,{message:"Site title too long"}),description:o.string().max(500,{message:"Description too long"}).optional(),base:o.string().startsWith("/",{message:"Base must start with /"}).optional(),lang:o.string().regex(/^[a-z]{2}(-[A-Z]{2})?$/,{message:"Invalid language code"}).optional(),head:o.array(
|
|
1
|
+
import{z as o}from"zod";const e=o.object({type:o.enum(["meta","link","script","base"])}).passthrough(),t=o.object({title:o.string().min(1,{message:"Site title is required"}).max(100,{message:"Site title too long"}),description:o.string().max(500,{message:"Description too long"}).optional(),base:o.string().startsWith("/",{message:"Base must start with /"}).optional(),lang:o.string().regex(/^[a-z]{2}(-[A-Z]{2})?$/,{message:"Invalid language code"}).optional(),head:o.array(e).optional()}),a=o.union([o.string(),o.object({dir:o.string().optional(),sidebar:o.boolean().optional(),toc:o.boolean().optional(),rss:o.boolean().optional(),list:o.boolean().optional()}).passthrough()]),n=o.record(o.string(),a),i=o.object({text:o.string().min(1,{message:"Nav item text is required"}),link:o.string().min(1,{message:"Nav item link is required"})}),s=o.object({text:o.string().min(1,{message:"Sidebar item text is required"}),link:o.string().min(1,{message:"Sidebar item link is required"})}),r=o.record(o.string(),o.union([o.array(s),o.object({autoGenerate:o.boolean()})])),l=o.object({navbar:o.array(i).optional(),sidebar:r.optional()}).passthrough(),p=o.object({name:o.string().regex(/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,{message:"Invalid theme name"}).refine(o=>!o.includes(".."),{message:"Theme name cannot contain path traversal"}).optional(),designSystem:o.enum(["vitepress","docusaurus","rspress"]).optional(),options:l.optional()}),g=o.object({lineNumbers:o.boolean().optional(),theme:o.object({light:o.string().optional(),dark:o.string().optional()}).optional(),markoTags:o.object({enabled:o.boolean().optional(),tagsDir:o.string().optional()}).optional()}),m=o.object({useCatchAllRoutes:o.boolean().optional(),outDir:o.string().optional(),assetsDir:o.string().optional(),sourcemap:o.boolean().optional(),minify:o.boolean().optional(),clean:o.boolean().optional()}).passthrough(),c=o.union([o.string(),o.tuple([o.string(),o.record(o.string(),o.unknown()).optional()]),o.object({name:o.string().min(1,{message:"Plugin name is required"}),options:o.record(o.string(),o.unknown()).optional()})]),u=o.object({hostname:o.string().optional(),exclude:o.array(o.string()).optional(),transformItems:o.any().optional()}).passthrough(),b=o.object({sitemap:u.optional()}).passthrough().optional();export const MarkoPressConfigSchema=o.object({site:t,contentDir:o.string().optional(),content:n.optional(),theme:p.optional(),markdown:g.optional(),build:m.optional(),search:o.object({enabled:o.boolean().optional()}).passthrough().optional(),seo:b,plugins:o.array(c).optional()});export function validateConfig(o){return MarkoPressConfigSchema.parse(o)}export function validateConfigSafe(o){const e=MarkoPressConfigSchema.safeParse(o);return e.success?{success:!0,data:e.data}:{success:!1,errors:e.error.issues.map(o=>({path:o.path.join("."),message:o.message}))}}
|
package/dist/dev/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{promises as e}from"node:fs";import o from"node:path";import{spawn as t}from"node:child_process";import r from"gray-matter";import{loadConfig as n}from"../config/index.js";import{PluginManager as s}from"../plugin/manager.js";import{generateRoutes as i,copyThemeCSS as a,generateCatchAllRoutes as c,filePathToUrl as l,extractStylesFromMarkoTags as d}from"../build/index.js";import{buildSearchIndex as m}from"../search/index.js";import{renderMarkdown as p}from"../markdown/renderer.js";export async function startDevServer(r={}){console.log("š Starting MarkoPress dev server...\n");const g=r.root||process.cwd()
|
|
1
|
+
import{promises as e}from"node:fs";import o from"node:path";import{spawn as t}from"node:child_process";import r from"gray-matter";import{loadConfig as n}from"../config/index.js";import{PluginManager as s}from"../plugin/manager.js";import{generateRoutes as i,copyThemeCSS as a,generateCatchAllRoutes as c,filePathToUrl as l,extractStylesFromMarkoTags as d}from"../build/index.js";import{buildSearchIndex as m}from"../search/index.js";import{renderMarkdown as p}from"../markdown/renderer.js";export async function startDevServer(r={}){console.log("š Starting MarkoPress dev server...\n");const g=r.root||process.cwd();let u=await n(g,{mode:"development",command:"dev"});console.log("ā Config loaded from "+u.root);const h=o.join(g,".markopress"),w=await e.stat(h).then(e=>e.isDirectory()).catch(()=>!1)?h:g;let f;u.plugins&&u.plugins.length>0&&(f=new s(u),await f.loadPlugins(u.plugins),u=f.getConfig(),await f.execLoadContentHooks());const v=o.join(w,"src",".generated","markdown");await e.rm(v,{recursive:!0,force:!0});const x={},k=[],y=o.resolve(g,u.contentDir);try{const t=await e.readdir(y,{withFileTypes:!0,recursive:!0}),r=new Map;for(const e of t){if(!e.isFile()||!e.name.endsWith(".md"))continue;const t=o.join(e.path||e.parentPath||y,e.name),n=o.relative(y,t),s=l(t,y),i=n.split(o.sep),a=1===i.length?"root":i[0];r.has(a)||r.set(a,[]),r.get(a).push({id:e.name.replace(".md",""),slug:e.name.replace(".md",""),filePath:t,urlPath:s,directory:a})}for(const[e,t]of r)k.push({id:e,dir:o.join(y,"root"===e?"":e),files:t})}catch(e){console.warn("Warning: Could not scan content directory: "+e)}console.log("š Generating routes from content...");const j=o.join(w,"src","routes"),S=r.useCatchAllRoutes??u.build.useCatchAllRoutes;await e.mkdir(j,{recursive:!0});let C={};if(f&&(C=await f.execExtendRoutesHooks(C)),S?(await c(x,j,u,k,!1,!1),console.log(" Using catch-all dynamic routes")):(await i(x,j,u,k,!1),console.log(" Using static routes")),console.log(" Routes generated\n"),f&&await f.execAllContentLoadedHooks(C),console.log("šØ Copying theme CSS..."),await a(w,u,!1),console.log(" Theme CSS copied\n"),u.markdown?.markoTags?.enabled&&(console.log("šØ Extracting custom tag styles..."),await d(w,u,!1),console.log(" Custom tag styles extracted\n")),!1!==u.search?.enabled){console.log("š Building search index...");const t=[],r=o.resolve(g,u.contentDir);try{const n=await e.readdir(r,{withFileTypes:!0,recursive:!0});for(const s of n){if(!s.isFile()||!s.name.endsWith(".md"))continue;const n=o.join(s.path||s.parentPath||r,s.name),i=l(n,r),a=await e.readFile(n,"utf-8"),c=await p(a,u.markdown),d=s.name.replace(".md","");t.push({url:i,html:c.html,title:c.frontmatter?.title||d,frontmatter:c.frontmatter})}}catch{}try{const r=await m(t,u.search),n=o.join(w,"public","search-index.json");await e.mkdir(o.dirname(n),{recursive:!0}),await e.writeFile(n,r),console.log(` Search index built (${t.length} pages)\n`)}catch(e){console.warn(" Warning: Failed to build search index:",e)}}console.log("šØ Starting @marko/run dev server...\n");const F=r.port||3e3,T=["dev"];F&&T.push("--port",F+"");const b=t("npx",["marko-run",...T],{stdio:"inherit",cwd:w});b.on("error",e=>{console.error("Failed to start dev server:",e),process.exit(1)}),b.on("exit",e=>{0!==e&&(console.error("Dev server exited with code "+e),process.exit(e||1))}),process.on("SIGINT",()=>{b.kill("SIGINT"),process.exit(0)}),process.on("SIGTERM",()=>{b.kill("SIGTERM"),process.exit(0)})}
|
package/dist/plugin/manager.d.ts
CHANGED
|
@@ -96,6 +96,10 @@ export declare class PluginManager {
|
|
|
96
96
|
* Get plugin data
|
|
97
97
|
*/
|
|
98
98
|
getPluginData(): Map<string, unknown>;
|
|
99
|
+
/**
|
|
100
|
+
* Get the latest resolved config after plugin config hooks
|
|
101
|
+
*/
|
|
102
|
+
getConfig(): ResolvedConfig;
|
|
99
103
|
/**
|
|
100
104
|
* Clear plugin state
|
|
101
105
|
*/
|
package/dist/plugin/manager.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"markdown-it";import{AllContentImpl as n,ContentActionsImpl as e}from"./context.js";import{wrapLegacyPlugin as o}from"./compat.js";export class PluginManager{plugins=[];config;pluginLoadOrder=[];loadResults=[];allContent=new n;contentActions=new e;constructor(t){this.config=t}async loadPlugins(t){const n=await this.resolvePluginDependencies(t);for(const t of n){const n=await this.loadPlugin(t);if(n){const t=o(n.plugin);this.plugins.push(t),this.loadResults.push(n),this.pluginLoadOrder.push(t.name)}}console.log(`ā Loaded ${this.plugins.length} plugins: ${this.pluginLoadOrder.join(", ")}`)}async resolvePluginDependencies(t){const n=new Set,e=new Set,o=[],i=new Map;for(const n of t){const t=this.getPluginName(n);i.set(t,n)}const s=async t=>{if(n.has(t))return;if(e.has(t))throw Error("Circular dependency detected among plugins: "+t);e.add(t);const a=i.get(t);if(!a)throw Error("Plugin not found: "+t);const r=await this.getPluginDependencies(a);for(const t of r)i.has(t)&&await s(t);e.delete(t),n.add(t),o.push(a)};for(const t of i.keys())await s(t);return o}getPluginName(t){return"string"==typeof t?t:Array.isArray(t)?t[0]:t.name}async getPluginDependencies(t){try{const n=await this.loadPluginModule(t);if(n&&n.dependencies)return n.dependencies}catch{}return[]}resolvePluginPath(t){return["sidenav","toc","blog-index","seo"].includes(t)?`../plugins/${t}/index.js`:t}async loadPluginModule(t){try{if("string"==typeof t)return(await import(this.resolvePluginPath(t))).default;if(Array.isArray(t)){const[n]=t;return(await import(this.resolvePluginPath(n))).default}return t}catch{return null}}async loadPlugin(t){const n=this.getPluginName(t);try{let e;if("string"==typeof t){const n=await import(this.resolvePluginPath(t));e="function"==typeof n.default?await n.default():n.default}else if(Array.isArray(t)){const[n,o]=t,i=await import(this.resolvePluginPath(n));e="function"==typeof i.default?await i.default(o):i.default}else e=t;if(!e||!e.name)throw Error(`Invalid plugin: ${n} - must have a 'name' property`);if(e.config){const t={...this.config};try{this.config=await e.config(this.config)}catch(n){console.error(`Plugin ${e.name} config hook failed, using original config:`,n),this.config=t}}return{plugin:e,config:t}}catch(e){const o=e instanceof Error?e.message:e+"";return console.error(`Failed to load plugin ${n}: ${o}`),{plugin:{name:n},config:t,error:e instanceof Error?e:Error(o)}}}getPlugins(){return this.plugins}getLoadResults(){return this.loadResults}async execMarkdownHooks(t){for(const n of this.plugins)if(n.extendMarkdown)try{await n.extendMarkdown(t)}catch(t){console.error(`Plugin ${n.name} extendMarkdown hook failed:`,t)}}async execExtendRoutesHooks(t){let n=t;for(const t of this.plugins)if(t.extendRoutes)try{n=await t.extendRoutes(n)||n}catch(n){console.error(`Plugin ${t.name} extendRoutes hook failed:`,n)}return n}async execLoadContentHooks(){for(const t of this.plugins)if(t.loadContent)try{const n=await t.loadContent();this.allContent.addPluginContent(t.name,n)}catch(n){console.error(`Plugin ${t.name} loadContent hook failed:`,n)}}async execEnhanceModulesHooks(t){for(const n of this.plugins)if(n.enhanceModules)try{let e=t;n.modules&&n.modules.length>0&&(e=t.filter(t=>n.modules.includes(t.id))),await n.enhanceModules(e)}catch(t){console.error(`Plugin ${n.name} enhanceModules hook failed:`,t)}}async execContentLoadedHooks(t){for(const t of this.plugins)if(t.contentLoaded)try{const n=this.allContent.getContent(t.name)[0]||{};await t.contentLoaded({content:n,allContent:this.allContent,actions:this.contentActions})}catch(n){console.error(`Plugin ${t.name} contentLoaded hook failed:`,n)}}async execAllContentLoadedHooks(t){for(const n of this.plugins)if(n.allContentLoaded)try{await n.allContentLoaded({allContent:this.allContent,routes:t,actions:this.contentActions})}catch(t){console.error(`Plugin ${n.name} allContentLoaded hook failed:`,t)}}async execPostBuildHooks(t,n,e,o){for(const i of this.plugins)if(i.postBuild)try{await i.postBuild({config:t,outDir:n,routes:e,assets:o,allContent:this.allContent})}catch(t){console.error(`Plugin ${i.name} postBuild hook failed:`,t)}}getPluginRoutes(){return this.contentActions.getAllRoutes()}getPluginData(){return this.contentActions.getData()}clear(){this.allContent=new n,this.contentActions=new e}createPluginContext(){return{config:this.config,utils:{log:t=>console.log("[markopress] "+t),warn:t=>console.warn("[markopress] "+t),error:t=>console.error("[markopress] "+t)}}}createBuildContext(t,n){return{...this.createPluginContext(),content:t,routes:n}}}
|
|
1
|
+
import t from"markdown-it";import{AllContentImpl as n,ContentActionsImpl as e}from"./context.js";import{wrapLegacyPlugin as o}from"./compat.js";export class PluginManager{plugins=[];config;pluginLoadOrder=[];loadResults=[];allContent=new n;contentActions=new e;constructor(t){this.config=t}async loadPlugins(t){const n=await this.resolvePluginDependencies(t);for(const t of n){const n=await this.loadPlugin(t);if(n){const t=o(n.plugin);this.plugins.push(t),this.loadResults.push(n),this.pluginLoadOrder.push(t.name)}}console.log(`ā Loaded ${this.plugins.length} plugins: ${this.pluginLoadOrder.join(", ")}`)}async resolvePluginDependencies(t){const n=new Set,e=new Set,o=[],i=new Map;for(const n of t){const t=this.getPluginName(n);i.set(t,n)}const s=async t=>{if(n.has(t))return;if(e.has(t))throw Error("Circular dependency detected among plugins: "+t);e.add(t);const a=i.get(t);if(!a)throw Error("Plugin not found: "+t);const r=await this.getPluginDependencies(a);for(const t of r)i.has(t)&&await s(t);e.delete(t),n.add(t),o.push(a)};for(const t of i.keys())await s(t);return o}getPluginName(t){return"string"==typeof t?t:Array.isArray(t)?t[0]:t.name}async getPluginDependencies(t){try{const n=await this.loadPluginModule(t);if(n&&n.dependencies)return n.dependencies}catch{}return[]}resolvePluginPath(t){return["sidenav","toc","blog-index","seo","head-inject"].includes(t)?`../plugins/${t}/index.js`:t}async loadPluginModule(t){try{if("string"==typeof t)return(await import(this.resolvePluginPath(t))).default;if(Array.isArray(t)){const[n]=t;return(await import(this.resolvePluginPath(n))).default}return t}catch{return null}}async loadPlugin(t){const n=this.getPluginName(t);try{let e;if("string"==typeof t){const n=await import(this.resolvePluginPath(t));e="function"==typeof n.default?await n.default():n.default}else if(Array.isArray(t)){const[n,o]=t,i=await import(this.resolvePluginPath(n));e="function"==typeof i.default?await i.default(o):i.default}else e=t;if(!e||!e.name)throw Error(`Invalid plugin: ${n} - must have a 'name' property`);if(e.config){const t={...this.config};try{this.config=await e.config(this.config)}catch(n){console.error(`Plugin ${e.name} config hook failed, using original config:`,n),this.config=t}}return{plugin:e,config:t}}catch(e){const o=e instanceof Error?e.message:e+"";return console.error(`Failed to load plugin ${n}: ${o}`),{plugin:{name:n},config:t,error:e instanceof Error?e:Error(o)}}}getPlugins(){return this.plugins}getLoadResults(){return this.loadResults}async execMarkdownHooks(t){for(const n of this.plugins)if(n.extendMarkdown)try{await n.extendMarkdown(t)}catch(t){console.error(`Plugin ${n.name} extendMarkdown hook failed:`,t)}}async execExtendRoutesHooks(t){let n=t;for(const t of this.plugins)if(t.extendRoutes)try{n=await t.extendRoutes(n)||n}catch(n){console.error(`Plugin ${t.name} extendRoutes hook failed:`,n)}return n}async execLoadContentHooks(){for(const t of this.plugins)if(t.loadContent)try{const n=await t.loadContent();this.allContent.addPluginContent(t.name,n)}catch(n){console.error(`Plugin ${t.name} loadContent hook failed:`,n)}}async execEnhanceModulesHooks(t){for(const n of this.plugins)if(n.enhanceModules)try{let e=t;n.modules&&n.modules.length>0&&(e=t.filter(t=>n.modules.includes(t.id))),await n.enhanceModules(e)}catch(t){console.error(`Plugin ${n.name} enhanceModules hook failed:`,t)}}async execContentLoadedHooks(t){for(const t of this.plugins)if(t.contentLoaded)try{const n=this.allContent.getContent(t.name)[0]||{};await t.contentLoaded({content:n,allContent:this.allContent,actions:this.contentActions})}catch(n){console.error(`Plugin ${t.name} contentLoaded hook failed:`,n)}}async execAllContentLoadedHooks(t){for(const n of this.plugins)if(n.allContentLoaded)try{await n.allContentLoaded({allContent:this.allContent,routes:t,actions:this.contentActions})}catch(t){console.error(`Plugin ${n.name} allContentLoaded hook failed:`,t)}}async execPostBuildHooks(t,n,e,o){for(const i of this.plugins)if(i.postBuild)try{await i.postBuild({config:t,outDir:n,routes:e,assets:o,allContent:this.allContent})}catch(t){console.error(`Plugin ${i.name} postBuild hook failed:`,t)}}getPluginRoutes(){return this.contentActions.getAllRoutes()}getPluginData(){return this.contentActions.getData()}getConfig(){return this.config}clear(){this.allContent=new n,this.contentActions=new e}createPluginContext(){return{config:this.config,utils:{log:t=>console.log("[markopress] "+t),warn:t=>console.warn("[markopress] "+t),error:t=>console.error("[markopress] "+t)}}}createBuildContext(t,n){return{...this.createPluginContext(),content:t,routes:n}}}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MarkoPressPlugin } from '../../plugin/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Head inject plugin configuration options
|
|
4
|
+
*/
|
|
5
|
+
export interface HeadInjectPluginOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Enable/disable the plugin
|
|
8
|
+
* @default true
|
|
9
|
+
*/
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Head Inject Plugin
|
|
14
|
+
*
|
|
15
|
+
* Injects custom head tags (meta, link, script, base) into pages
|
|
16
|
+
* based on site configuration (config.site.head).
|
|
17
|
+
*
|
|
18
|
+
* Reads head tags from config.site.head via the config hook,
|
|
19
|
+
* validates and transforms them, and stores them for rendering.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // In markopress config
|
|
23
|
+
* export default defineConfig({
|
|
24
|
+
* site: {
|
|
25
|
+
* head: [
|
|
26
|
+
* { type: 'meta', property: 'og:title', content: 'My Site' },
|
|
27
|
+
* { type: 'link', rel: 'preconnect', href: 'https://fonts.googleapis.com' }
|
|
28
|
+
* ]
|
|
29
|
+
* }
|
|
30
|
+
* })
|
|
31
|
+
*/
|
|
32
|
+
export declare const headInjectPlugin: MarkoPressPlugin;
|
|
33
|
+
/**
|
|
34
|
+
* Default export for plugin loading
|
|
35
|
+
*/
|
|
36
|
+
export default headInjectPlugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{validateHeadConfig as e}from"./validator.js";import{transformHeadConfig as t}from"./transformer.js";export const headInjectPlugin={name:"head-inject",config(n){const r=n.site?.head;if(!r||0===r.length)return n;e(r);const o=t(r);return{...n,_headInject:o}}};export default headInjectPlugin;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transformer for head tags
|
|
3
|
+
* Converts HeadTag objects to renderable format for Marko templates
|
|
4
|
+
*/
|
|
5
|
+
import type { HeadTag, GroupedHeadTags } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Remove undefined values from an object
|
|
8
|
+
*/
|
|
9
|
+
export declare function filterUndefined<T extends Record<string, unknown>>(obj: T): Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Transform head configuration to grouped renderable format
|
|
12
|
+
*/
|
|
13
|
+
export declare function transformHeadConfig(tags: HeadTag[]): GroupedHeadTags;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function filterUndefined(e){const t={};for(const[n,r]of Object.entries(e))void 0!==r&&(t[n]=r);return t}function e(e){switch(e.type){case"meta":return function(e){return e.charset?["meta",{charset:e.charset}]:["meta",filterUndefined({name:e.name,property:e.property,"http-equiv":e.httpEquiv,content:e.content})]}(e);case"link":return function(e){return["link",filterUndefined({rel:e.rel,href:e.href,as:e.as,type:e.mimeType,media:e.media,sizes:e.sizes,crossorigin:e.crossorigin,integrity:e.integrity,disabled:e.disabled,title:e.title})]}(e);case"script":return function(e){const t=filterUndefined({src:e.src,async:e.async,defer:e.defer,type:e.scriptType,crossorigin:e.crossorigin,integrity:e.integrity,nonce:e.nonce,...e.attrs});return e.content?["script",t,e.content]:["script",t]}(e);case"base":return function(e){return["base",filterUndefined({href:e.href,target:e.target})]}(e);default:throw Error(`[head-inject] Unknown tag type '${e.type}'`)}}export function transformHeadConfig(t){const n=[],r=[];for(const i of t){const t=e(i);"top"===i.position?n.push(t):r.push(t)}return{headTop:n,headBottom:r}}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Head injection plugin types
|
|
3
|
+
* Type-safe definitions for head tags with configurable positioning
|
|
4
|
+
*/
|
|
5
|
+
export type HeadTag = MetaTag | LinkTag | ScriptTag | BaseTag;
|
|
6
|
+
export interface BaseHeadTag {
|
|
7
|
+
/**
|
|
8
|
+
* Position in head element
|
|
9
|
+
* - 'top': renders in <theme-head-top/> slot (early in head)
|
|
10
|
+
* - 'bottom': renders in <theme-head-bottom/> slot (late in head)
|
|
11
|
+
* - undefined: defaults to 'bottom'
|
|
12
|
+
*/
|
|
13
|
+
position?: 'top' | 'bottom';
|
|
14
|
+
}
|
|
15
|
+
export interface MetaTag extends BaseHeadTag {
|
|
16
|
+
type: 'meta';
|
|
17
|
+
name?: string;
|
|
18
|
+
property?: string;
|
|
19
|
+
httpEquiv?: string;
|
|
20
|
+
content?: string;
|
|
21
|
+
charset?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface LinkTag extends BaseHeadTag {
|
|
24
|
+
type: 'link';
|
|
25
|
+
rel: string;
|
|
26
|
+
href: string;
|
|
27
|
+
as?: string;
|
|
28
|
+
mimeType?: string;
|
|
29
|
+
media?: string;
|
|
30
|
+
sizes?: string;
|
|
31
|
+
crossorigin?: 'anonymous' | 'use-credentials';
|
|
32
|
+
integrity?: string;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
title?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ScriptTag extends BaseHeadTag {
|
|
37
|
+
type: 'script';
|
|
38
|
+
src?: string;
|
|
39
|
+
content?: string;
|
|
40
|
+
async?: boolean;
|
|
41
|
+
defer?: boolean;
|
|
42
|
+
scriptType?: string;
|
|
43
|
+
crossorigin?: 'anonymous' | 'use-credentials';
|
|
44
|
+
integrity?: string;
|
|
45
|
+
nonce?: string;
|
|
46
|
+
attrs?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
export interface BaseTag extends BaseHeadTag {
|
|
49
|
+
type: 'base';
|
|
50
|
+
href: string;
|
|
51
|
+
target?: '_blank' | '_self' | '_parent' | '_top';
|
|
52
|
+
}
|
|
53
|
+
export type RenderableHeadTag = [string, Record<string, unknown>] | [string, Record<string, unknown>, string];
|
|
54
|
+
export interface GroupedHeadTags {
|
|
55
|
+
headTop: RenderableHeadTag[];
|
|
56
|
+
headBottom: RenderableHeadTag[];
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class t extends Error{constructor(t,e){super(`[head-inject] ${t} tag: ${e}`),this.name="ValidationError"}}export function validateHeadTag(e){if(e.position&&!["top","bottom"].includes(e.position))throw new t(e.type,`Invalid position '${e.position}', must be 'top' or 'bottom'`);switch(e.type){case"meta":!function(e){if(e.charset)return;if(!e.content)throw new t("meta","Missing required attribute: content");if(!(e.name||e.property||e.httpEquiv))throw new t("meta","Must have one of: name, property, or httpEquiv")}(e);break;case"link":!function(e){if(!e.rel)throw new t("link","Missing required attribute: rel");if(!e.href)throw new t("link","Missing required attribute: href")}(e);break;case"script":!function(e){const r=void 0!==e.src,o=void 0!==e.content;if(!r&&!o)throw new t("script","Must have either src or content");if(r&&o)throw new t("script","src and content are mutually exclusive")}(e);break;case"base":!function(e){if(!e.href)throw new t("base","Missing required attribute: href")}(e);break;default:throw new t("unknown",`Unknown tag type '${e.type}'`)}}export function validateHeadConfig(t){if(t.length>0&&Array.isArray(t[0]))throw Error('[head-inject] Invalid config format. Head tags must be objects with a "type" property, not arrays. See documentation for the new format.');let e=0;for(const r of t){if("object"!=typeof r||null===r)throw Error("[head-inject] Invalid head tag: must be an object");"base"===r.type&&e++}if(e>1)throw Error("[head-inject] Only one <base> tag is allowed per page");for(const e of t)validateHeadTag(e)}
|
package/dist/preview/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import t from"node:path";import{createReadStream as e,existsSync as o,statSync as r}from"node:fs";import{promises as s}from"node:fs";import{createServer as n}from"node:http";import{loadConfig as i}from"../config/index.js";const a={".html":"text/html; charset=utf-8",".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json; charset=utf-8",".xml":"application/xml; charset=utf-8",".txt":"text/plain; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".gif":"image/gif",".ico":"image/x-icon",".woff":"font/woff",".woff2":"font/woff2"};function c(e,o){const r=o.replace(/^\/+/,""),s=t.join(e,r),n=t.relative(e,s);return n.startsWith("..")||t.isAbsolute(n)?null:s}export async function preview(l={}){const{port:p=4173,host:f="localhost",root:h}=l,m=h||process.cwd(),u=await i(m,{mode:"production",command:"preview"}),d=(u.site?.base||"/").replace(/\/?$/,"/"),g=t.join(m,".markopress"),w=o(g)&&r(g).isDirectory()?g:m,x=u.build?.outDir||"dist",v=t.join(w,x,"public");try{if(!(await s.stat(v)).isDirectory())throw Error()}catch{console.error("Preview build output not found at: "+v),console.error('Run "markopress build" before "markopress preview".'),process.exit(1)}const j=d.replace(/\/$/,""),y=n((o,r)=>{const n=o.method||"GET";if("GET"!==n&&"HEAD"!==n)return r.writeHead(405),void r.end("Method Not Allowed");let i=new URL(o.url||"/",`http://${f}:${p}`).pathname||"/";j&&i.startsWith(j+"/")?i=i.slice(j.length)||"/":j&&i===j&&(i="/"),async function(e,o){for(const r of function(e){return"/"===e?["/index.html"]:e.endsWith("/")?[e+"index.html"]:t.posix.extname(e)?[e]:[e+".html",e+"/index.html"]}(o)){const t=c(e,r);if(t)try{if((await s.stat(t)).isFile())return{filePath:t,statusCode:200}}catch{}}const r=c(e,"/404.html");if(!r)return null;try{if((await s.stat(r)).isFile())return{filePath:r,statusCode:404}}catch{}return null}(v,decodeURIComponent(i)).then(o=>{if(!o)return r.writeHead(404,{"content-type":"text/plain; charset=utf-8"}),void r.end("Not Found");const s=a[t.extname(o.filePath)]||"application/octet-stream";r.writeHead(o.statusCode,{"content-type":s,"cache-control":"no-cache"}),"HEAD"!==n?e(o.filePath).pipe(r):r.end()}).catch(()=>{r.writeHead(500,{"content-type":"text/plain; charset=utf-8"}),r.end("Internal Server Error")})});return await new Promise((t,e)=>{y.once("error",e),y.listen(p,f,()=>t())}),console.log("š Starting MarkoPress preview server...\n"),j?(console.log(` Server: http://${f}:${p}${j}/`),console.log(" Base path: "+d)):console.log(` Server: http://${f}:${p}`),console.log(" Serving: "+v),console.log(" Press Ctrl+C to stop\n"),process.on("SIGINT",()=>{y.close(),process.exit(0)}),process.on("SIGTERM",()=>{y.close(),process.exit(0)}),new Promise(()=>{})}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markopress",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "A fast, modern static site generator built on Marko.js v6 - drop-in alternative to VitePress and Docusaurus with full content compatibility",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"static-site-generator",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>${$global.title || 'MarkoPress Site'}</title>
|
|
7
|
+
<if=$global.description>
|
|
8
|
+
<meta name="description" content=$global.description>
|
|
9
|
+
</if>
|
|
10
|
+
|
|
11
|
+
<!-- Inject head tags from frontmatter (top position) -->
|
|
12
|
+
<if=$global.headTop>
|
|
13
|
+
<for|tag| of=$global.headTop>
|
|
14
|
+
<if=tag.length === 3>
|
|
15
|
+
<!-- Tag with content (e.g., <script>content</script>) -->
|
|
16
|
+
<${tag[0]} ...tag[1]>
|
|
17
|
+
${tag[2]}
|
|
18
|
+
</${tag[0]}>
|
|
19
|
+
<else>
|
|
20
|
+
<!-- Self-closing tag -->
|
|
21
|
+
<${tag[0]} ...tag[1]/>
|
|
22
|
+
</if>
|
|
23
|
+
</for>
|
|
24
|
+
</if>
|
|
25
|
+
|
|
26
|
+
<link rel="stylesheet" href="/theme.css">
|
|
27
|
+
|
|
28
|
+
<!-- Inject head tags from frontmatter (bottom position) -->
|
|
29
|
+
<if=$global.headBottom>
|
|
30
|
+
<for|tag| of=$global.headBottom>
|
|
31
|
+
<if=tag.length === 3>
|
|
32
|
+
<!-- Tag with content (e.g., <script>content</script>) -->
|
|
33
|
+
<${tag[0]} ...tag[1]>
|
|
34
|
+
${tag[2]}
|
|
35
|
+
</${tag[0]}>
|
|
36
|
+
<else>
|
|
37
|
+
<!-- Self-closing tag -->
|
|
38
|
+
<${tag[0]} ...tag[1]/>
|
|
39
|
+
</if>
|
|
40
|
+
</for>
|
|
41
|
+
</if>
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
44
|
+
<div class="app-container">
|
|
45
|
+
<!-- Navbar -->
|
|
46
|
+
<nav class="navbar">
|
|
47
|
+
<div class="navbar-container">
|
|
48
|
+
<a href="/" class="navbar-brand">
|
|
49
|
+
<span class="navbar-title">MarkoPress Site</span>
|
|
50
|
+
</a>
|
|
51
|
+
<if=$global.navbar>
|
|
52
|
+
<nav class="navbar-nav">
|
|
53
|
+
<for|item| of=$global.navbar>
|
|
54
|
+
<a href=item.link class="nav-link">${item.text}</a>
|
|
55
|
+
</for>
|
|
56
|
+
</nav>
|
|
57
|
+
</if>
|
|
58
|
+
</div>
|
|
59
|
+
</nav>
|
|
60
|
+
|
|
61
|
+
<!-- Main content area with optional sidebar -->
|
|
62
|
+
<div class="main-wrapper">
|
|
63
|
+
<if=$global.sidebar>
|
|
64
|
+
<!-- Sidebar for docs -->
|
|
65
|
+
<aside class="sidebar">
|
|
66
|
+
<nav class="sidebar-nav">
|
|
67
|
+
<for|item| of=$global.sidebar>
|
|
68
|
+
<a href=item.link class="sidebar-link">${item.text}</a>
|
|
69
|
+
</for>
|
|
70
|
+
</nav>
|
|
71
|
+
</aside>
|
|
72
|
+
</if>
|
|
73
|
+
|
|
74
|
+
<!-- Page content -->
|
|
75
|
+
<main class="main-content">
|
|
76
|
+
<${input.content}/>
|
|
77
|
+
</main>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<!-- Footer -->
|
|
81
|
+
<footer class="footer">
|
|
82
|
+
<div class="footer-container">
|
|
83
|
+
<p>© 2024 MarkoPress Site. Built with MarkoPress.</p>
|
|
84
|
+
</div>
|
|
85
|
+
</footer>
|
|
86
|
+
</div>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
<if=$global.headTop && $global.headTop.length>
|
|
5
5
|
<for|tag| of=$global.headTop>
|
|
6
6
|
<if=tag.length === 3>
|
|
7
|
-
<!-- Inline content tag: [tagName, attrs, content] -->
|
|
8
7
|
<${tag[0]} ...tag[1]>
|
|
9
8
|
${tag[2]}
|
|
10
9
|
</${tag[0]}>
|
|
10
|
+
</if>
|
|
11
11
|
<else>
|
|
12
|
-
<!-- Self-closing tag: [tagName, attrs] -->
|
|
13
12
|
<${tag[0]} ...tag[1]/>
|
|
14
|
-
</
|
|
13
|
+
</else>
|
|
15
14
|
</for>
|
|
16
15
|
</if>
|