diff --git a/build.zig b/build.zig index 391c153..8407f87 100644 --- a/build.zig +++ b/build.zig @@ -44,6 +44,8 @@ pub fn build(b: *std.Build) !void { // b.dependency("jetzig",.{}).builder.dependency("zmpl",.{}).module("zmpl"); b.modules.put("zmpl", zmpl_dep.module("zmpl")) catch @panic("Out of memory"); + const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize }); + const ZmplBuild = @import("zmpl").ZmplBuild; const ZmplTemplate = @import("zmpl").Template; var zmpl_build = ZmplBuild.init(b, lib, template_path); @@ -60,6 +62,7 @@ pub fn build(b: *std.Build) !void { lib.root_module.addImport("zmpl", zmpl_module); jetzig_module.addImport("zmpl", zmpl_module); jetzig_module.addImport("args", zig_args_dep.module("args")); + jetzig_module.addImport("zmd", zmd_dep.module("zmd")); const main_tests = b.addTest(.{ .root_source_file = .{ .path = "src/tests.zig" }, @@ -148,6 +151,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn exe_static_routes.root_module.addImport("routes", routes_module); exe_static_routes.root_module.addImport("jetzig", jetzig_module); exe_static_routes.root_module.addImport("zmpl", zmpl_module); + exe_static_routes.root_module.addImport("jetzig_app", &exe.root_module); const run_static_routes_cmd = b.addRunArtifact(exe_static_routes); exe.step.dependOn(&run_static_routes_cmd.step); diff --git a/build.zig.zon b/build.zig.zon index a8de725..8a1cfbe 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,6 +2,10 @@ .name = "jetzig", .version = "0.0.0", .dependencies = .{ + .zmd = .{ + .url = "https://github.com/jetzig-framework/zmd/archive/1a526ca1cf577789ca15ca1b71d3e943b81fb801.tar.gz", + .hash = "1220bfc5c29bc930b5a524c210712ef65c6cde6770450899bef01164a3089e6707fa", + }, .zmpl = .{ .url = "https://github.com/jetzig-framework/zmpl/archive/9e2df115c9f17e92fb60a4d09bf55ea48d0388b0.tar.gz", .hash = "1220820b7f5f3e01b7dc976d32cf9ff65d44dee2642533f4b8104e19a824e802d7e1", diff --git a/demo/public/prism.css b/demo/public/prism.css new file mode 100644 index 0000000..26d043a --- /dev/null +++ b/demo/public/prism.css @@ -0,0 +1,3 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+zig&plugins=file-highlight */ +code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green} diff --git a/demo/public/prism.js b/demo/public/prism.js new file mode 100644 index 0000000..74c62a7 --- /dev/null +++ b/demo/public/prism.js @@ -0,0 +1,10 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+zig&plugins=file-highlight */ +var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; +!function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); +Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; +Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; +Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json; +!function(e){function n(e){return function(){return e}}var r=/\b(?:align|allowzero|and|anyframe|anytype|asm|async|await|break|cancel|catch|comptime|const|continue|defer|else|enum|errdefer|error|export|extern|fn|for|if|inline|linksection|nakedcc|noalias|nosuspend|null|or|orelse|packed|promise|pub|resume|return|stdcallcc|struct|suspend|switch|test|threadlocal|try|undefined|union|unreachable|usingnamespace|var|volatile|while)\b/,a="\\b(?!"+r.source+")(?!\\d)\\w+\\b",o="align\\s*\\((?:[^()]|\\([^()]*\\))*\\)",s="(?!\\s)(?:!?\\s*(?:"+"(?:\\?|\\bpromise->|(?:\\[[^[\\]]*\\]|\\*(?!\\*)|\\*\\*)(?:\\s*|\\s*const\\b|\\s*volatile\\b|\\s*allowzero\\b)*)".replace(//g,n(o))+"\\s*)*"+"(?:\\bpromise\\b|(?:\\berror\\.)?(?:\\.)*(?!\\s+))".replace(//g,n(a))+")+";e.languages.zig={comment:[{pattern:/\/\/[/!].*/,alias:"doc-comment"},/\/{2}.*/],string:[{pattern:/(^|[^\\@])c?"(?:[^"\\\r\n]|\\.)*"/,lookbehind:!0,greedy:!0},{pattern:/([\r\n])([ \t]+c?\\{2}).*(?:(?:\r\n?|\n)\2.*)*/,lookbehind:!0,greedy:!0}],char:{pattern:/(^|[^\\])'(?:[^'\\\r\n]|[\uD800-\uDFFF]{2}|\\(?:.|x[a-fA-F\d]{2}|u\{[a-fA-F\d]{1,6}\}))'/,lookbehind:!0,greedy:!0},builtin:/\B@(?!\d)\w+(?=\s*\()/,label:{pattern:/(\b(?:break|continue)\s*:\s*)\w+\b|\b(?!\d)\w+\b(?=\s*:\s*(?:\{|while\b))/,lookbehind:!0},"class-name":[/\b(?!\d)\w+(?=\s*=\s*(?:(?:extern|packed)\s+)?(?:enum|struct|union)\s*[({])/,{pattern:RegExp("(:\\s*)(?=\\s*(?:\\s*)?[=;,)])|(?=\\s*(?:\\s*)?\\{)".replace(//g,n(s)).replace(//g,n(o))),lookbehind:!0,inside:null},{pattern:RegExp("(\\)\\s*)(?=\\s*(?:\\s*)?;)".replace(//g,n(s)).replace(//g,n(o))),lookbehind:!0,inside:null}],"builtin-type":{pattern:/\b(?:anyerror|bool|c_u?(?:int|long|longlong|short)|c_longdouble|c_void|comptime_(?:float|int)|f(?:16|32|64|128)|[iu](?:8|16|32|64|128|size)|noreturn|type|void)\b/,alias:"keyword"},keyword:r,function:/\b(?!\d)\w+(?=\s*\()/,number:/\b(?:0b[01]+|0o[0-7]+|0x[a-fA-F\d]+(?:\.[a-fA-F\d]*)?(?:[pP][+-]?[a-fA-F\d]+)?|\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)\b/,boolean:/\b(?:false|true)\b/,operator:/\.[*?]|\.{2,3}|[-=]>|\*\*|\+\+|\|\||(?:<<|>>|[-+*]%|[-+*/%^&|<>!=])=?|[?~]/,punctuation:/[.:,;(){}[\]]/},e.languages.zig["class-name"].forEach((function(n){null===n.inside&&(n.inside=e.languages.zig)}))}(Prism); +!function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector);var t={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"},e="data-src-status",i='pre[data-src]:not([data-src-status="loaded"]):not([data-src-status="loading"])';Prism.hooks.add("before-highlightall",(function(t){t.selector+=", "+i})),Prism.hooks.add("before-sanity-check",(function(a){var n=a.element;if(n.matches(i)){a.code="",n.setAttribute(e,"loading");var s=n.appendChild(document.createElement("CODE"));s.textContent="Loading…";var r=n.getAttribute("data-src"),l=a.language;if("none"===l){var o=(/\.(\w+)$/.exec(r)||[,"none"])[1];l=t[o]||o}Prism.util.setLanguage(s,l),Prism.util.setLanguage(n,l);var h=Prism.plugins.autoloader;h&&h.loadLanguages(l),function(t,i,a){var r=new XMLHttpRequest;r.open("GET",t,!0),r.onreadystatechange=function(){4==r.readyState&&(r.status<400&&r.responseText?function(t){n.setAttribute(e,"loaded");var i=function(t){var e=/^\s*(\d+)\s*(?:(,)\s*(?:(\d+)\s*)?)?$/.exec(t||"");if(e){var i=Number(e[1]),a=e[2],n=e[3];return a?n?[i,Number(n)]:[i,void 0]:[i,i]}}(n.getAttribute("data-range"));if(i){var a=t.split(/\r\n?|\n/g),r=i[0],l=null==i[1]?a.length:i[1];r<0&&(r+=a.length),r=Math.max(0,Math.min(r-1,a.length)),l<0&&(l+=a.length),l=Math.max(0,Math.min(l,a.length)),t=a.slice(r,l).join("\n"),n.hasAttribute("data-start")||n.setAttribute("data-start",String(r+1))}s.textContent=t,Prism.highlightElement(s)}(r.responseText):r.status>=400?a("✖ Error "+r.status+" while fetching file: "+r.statusText):a("✖ Error: File does not exist or is empty"))},r.send(null)}(r,0,(function(t){n.setAttribute(e,"failed"),s.textContent=t}))}})),Prism.plugins.fileHighlight={highlight:function(t){for(var e,a=(t||document).querySelectorAll(i),n=0;e=a[n++];)Prism.highlightElement(e)}};var a=!1;Prism.fileHighlight=function(){a||(console.warn("Prism.fileHighlight is deprecated. Use `Prism.plugins.fileHighlight.highlight` instead."),a=!0),Prism.plugins.fileHighlight.highlight.apply(this,arguments)}}}(); diff --git a/demo/src/app/views/layouts/application.zmpl b/demo/src/app/views/layouts/application.zmpl index 57aad59..71deebd 100644 --- a/demo/src/app/views/layouts/application.zmpl +++ b/demo/src/app/views/layouts/application.zmpl @@ -1,12 +1,15 @@ + +
{zmpl.content}
+ diff --git a/demo/src/app/views/markdown.zig b/demo/src/app/views/markdown.zig new file mode 100644 index 0000000..746c9f8 --- /dev/null +++ b/demo/src/app/views/markdown.zig @@ -0,0 +1,9 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +pub const layout = "application"; + +pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { + _ = data; + return request.render(.ok); +} diff --git a/demo/src/app/views/markdown/index.md b/demo/src/app/views/markdown/index.md new file mode 100644 index 0000000..1ab7e18 --- /dev/null +++ b/demo/src/app/views/markdown/index.md @@ -0,0 +1,66 @@ +# Markdown Example + +![jetzig logo](https://www.jetzig.dev/jetzig.png) + +_Markdown_ is rendered by _[zmd](https://github.com/jetzig-framework/zmd)_. + +You can use a `StaticRequest` in your view if you prefer to render your _Markdown_ at build time, or use `Request` in development to render at run time without a server restart. + +Simply create a `.md` file instead of a `.zmpl` file, e.g. `src/app/views/iguanas/index.md` and _Markdown_ will be rendered. + +## An _ordered_ list + +1. List item with a [link](https://ziglang.org/) +1. List item with some **bold** and _italic_ text +1. List item 3 + +## An _unordered_ list + +* List item 1 +* List item 2 +* List item 3 + +## Define your own formatters in `src/main.zig` + +```zig +pub const jetzig_options = struct { + pub const markdown_fragments = struct { + pub const root = .{ + "
", + "
", + }; + pub const h1 = .{ + "

", + "

", + }; + pub const h2 = .{ + "

", + "

", + }; + pub const h3 = .{ + "

", + "

", + }; + pub const paragraph = .{ + "

", + "

", + }; + pub const code = .{ + "", + "", + }; + + pub fn block(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 { + return try std.fmt.allocPrint(allocator, + \\
{s}
+ , .{try jetzig.zmd.html.escape(allocator, node.content)}); + } + + pub fn link(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 { + return try std.fmt.allocPrint(allocator, + \\{1s} + , .{ node.href.?, node.title.? }); + } + }; +} +``` diff --git a/demo/src/main.zig b/demo/src/main.zig index 268d2eb..6c5d3a0 100644 --- a/demo/src/main.zig +++ b/demo/src/main.zig @@ -1,6 +1,7 @@ const std = @import("std"); pub const jetzig = @import("jetzig"); + pub const routes = @import("routes").routes; // Override default settings in `jetzig.config` here: @@ -30,6 +31,57 @@ pub const jetzig_options = struct { // HTTP buffer. Must be large enough to store all headers. This should typically not be modified. // pub const http_buffer_size: usize = std.math.pow(usize, 2, 16); + + // Set custom fragments for rendering markdown templates. Any values will fall back to + // defaults provided by Zmd (https://github.com/bobf/zmd/blob/main/src/zmd/html.zig). + pub const markdown_fragments = struct { + pub const root = .{ + "
", + "
", + }; + pub const h1 = .{ + "

", + "

", + }; + pub const h2 = .{ + "

", + "

", + }; + pub const h3 = .{ + "

", + "

", + }; + pub const paragraph = .{ + "

", + "

", + }; + pub const code = .{ + "", + "", + }; + + pub const unordered_list = .{ + "
    ", + "
", + }; + + pub const ordered_list = .{ + "
    ", + "
", + }; + + pub fn block(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 { + return try std.fmt.allocPrint(allocator, + \\
{s}
+ , .{ node.meta, node.content }); + } + + pub fn link(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 { + return try std.fmt.allocPrint(allocator, + \\{1s} + , .{ node.href.?, node.title.? }); + } + }; }; pub fn main() !void { diff --git a/src/compile_static_routes.zig b/src/compile_static_routes.zig index 2fe1e12..b62c4b8 100644 --- a/src/compile_static_routes.zig +++ b/src/compile_static_routes.zig @@ -2,6 +2,7 @@ const std = @import("std"); const jetzig = @import("jetzig"); const routes = @import("routes").routes; const zmpl = @import("zmpl"); +const jetzig_options = @import("jetzig_app").jetzig_options; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; @@ -101,10 +102,60 @@ fn writeContent( std.debug.print("[jetzig] Compiled static route: {s}\n", .{json_path}); - if (zmpl.find(route.template)) |template| { - var content: []const u8 = undefined; - defer allocator.free(content); + const html_content = try renderZmplTemplate(allocator, route, view) orelse + try renderMarkdown(allocator, route, view) orelse + null; + const html_path = try std.mem.concat( + allocator, + u8, + &[_][]const u8{ route.name, index_suffix, ".html" }, + ); + if (html_content) |content| { + defer allocator.free(html_path); + const html_file = try dir.createFile(html_path, .{ .truncate = true }); + try html_file.writeAll(content); + defer html_file.close(); + allocator.free(content); + std.debug.print("[jetzig] Compiled static route: {s}\n", .{html_path}); + } +} +fn renderMarkdown( + allocator: std.mem.Allocator, + route: jetzig.views.Route, + view: jetzig.views.View, +) !?[]const u8 { + const fragments = if (@hasDecl(jetzig_options, "markdown_fragments")) + jetzig_options.markdown_fragments + else + null; + const content = try jetzig.markdown.render(allocator, &route, fragments) orelse return null; + + if (route.layout) |layout_name| { + try view.data.addConst("jetzig_view", view.data.string(route.name)); + try view.data.addConst("jetzig_action", view.data.string(@tagName(route.action))); + + // TODO: Allow user to configure layouts directory other than src/app/views/layouts/ + const prefixed_name = try std.mem.concat(allocator, u8, &[_][]const u8{ "layouts_", layout_name }); + defer allocator.free(prefixed_name); + defer allocator.free(prefixed_name); + + if (zmpl.find(prefixed_name)) |layout| { + view.data.content = .{ .data = content }; + return try layout.render(view.data); + } else { + std.debug.print("Unknown layout: {s}\n", .{layout_name}); + return content; + } + } else return null; +} + +fn renderZmplTemplate( + allocator: std.mem.Allocator, + route: jetzig.views.Route, + view: jetzig.views.View, +) !?[]const u8 { + if (zmpl.find(route.template)) |template| { try view.data.addConst("jetzig_view", view.data.string(route.name)); try view.data.addConst("jetzig_action", view.data.string(@tagName(route.action))); @@ -114,24 +165,13 @@ fn writeContent( defer allocator.free(prefixed_name); if (zmpl.find(prefixed_name)) |layout| { - content = try template.renderWithLayout(layout, view.data); + return try template.renderWithLayout(layout, view.data); } else { std.debug.print("Unknown layout: {s}\n", .{layout_name}); - content = try allocator.dupe(u8, ""); + return try allocator.dupe(u8, ""); } } else { - content = try template.render(view.data); + return try template.render(view.data); } - - const html_path = try std.mem.concat( - allocator, - u8, - &[_][]const u8{ route.name, index_suffix, ".html" }, - ); - defer allocator.free(html_path); - const html_file = try dir.createFile(html_path, .{ .truncate = true }); - try html_file.writeAll(content); - defer html_file.close(); - std.debug.print("[jetzig] Compiled static route: {s}\n", .{html_path}); - } + } else return null; } diff --git a/src/jetzig.zig b/src/jetzig.zig index df11219..dcb684c 100644 --- a/src/jetzig.zig +++ b/src/jetzig.zig @@ -1,6 +1,7 @@ const std = @import("std"); pub const zmpl = @import("zmpl").zmpl; +pub const zmd = @import("zmd").zmd; pub const http = @import("jetzig/http.zig"); pub const loggers = @import("jetzig/loggers.zig"); @@ -10,6 +11,7 @@ pub const colors = @import("jetzig/colors.zig"); pub const middleware = @import("jetzig/middleware.zig"); pub const util = @import("jetzig/util.zig"); pub const types = @import("jetzig/types.zig"); +pub const markdown = @import("jetzig/markdown.zig"); /// The primary interface for a Jetzig application. Create an `App` in your application's /// `src/main.zig` and call `start` to launch the application. @@ -65,6 +67,9 @@ pub const config = struct { /// modified. pub const http_buffer_size: usize = std.math.pow(usize, 2, 16); + /// A struct of fragments to use when rendering Markdown templates. + pub const markdown_fragments = zmd.html.DefaultFragments; + /// Reconciles a configuration value from user-defined values and defaults provided by Jetzig. pub fn get(T: type, comptime key: []const u8) T { const self = @This(); diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 8d94f18..dbc270d 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -108,11 +108,7 @@ fn renderResponse(self: *Self, request: *jetzig.http.Request) !void { if (isUnhandledError(err)) return err; const rendered = try self.renderInternalServerError(request, err); - - request.response.content = rendered.content; - request.response.status_code = rendered.view.status_code; - request.response.content_type = "text/html"; - + setResponse(request, rendered, .{}); return; }; @@ -130,6 +126,16 @@ fn renderResponse(self: *Self, request: *jetzig.http.Request) !void { } } +fn setResponse( + request: *jetzig.http.Request, + rendered_view: RenderedView, + options: struct { content_type: []const u8 = "text/html" }, +) void { + request.response.content = rendered_view.content; + request.response.status_code = rendered_view.view.status_code; + request.response.content_type = options.content_type; +} + fn renderStatic(resource: StaticResource, response: *jetzig.http.Response) !void { response.status_code = .ok; response.content = resource.content; @@ -146,12 +152,38 @@ fn renderHTML( const rendered = self.renderView(matched_route, request, template) catch |err| { if (isUnhandledError(err)) return err; const rendered_error = try self.renderInternalServerError(request, err); - request.response.content = rendered_error.content; - request.response.status_code = rendered_error.view.status_code; - request.response.content_type = "text/html"; + setResponse(request, rendered_error, .{}); return; }; - request.response.content = rendered.content; + setResponse(request, rendered, .{}); + return; + } else if (try jetzig.markdown.render(request.allocator, matched_route, null)) |markdown_content| { + const rendered = self.renderView(matched_route, request, null) catch |err| { + if (isUnhandledError(err)) return err; + const rendered_error = try self.renderInternalServerError(request, err); + setResponse(request, rendered_error, .{}); + return; + }; + + try addTemplateConstants(rendered.view, matched_route); + + if (request.getLayout(matched_route)) |layout_name| { + // TODO: Allow user to configure layouts directory other than src/app/views/layouts/ + const prefixed_name = try std.mem.concat( + self.allocator, + u8, + &[_][]const u8{ "layouts_", layout_name }, + ); + defer self.allocator.free(prefixed_name); + + if (zmpl.manifest.find(prefixed_name)) |layout| { + rendered.view.data.content = .{ .data = markdown_content }; + request.response.content = try layout.render(rendered.view.data); + } else { + try self.logger.WARN("Unknown layout: {s}", .{layout_name}); + request.response.content = markdown_content; + } + } request.response.status_code = rendered.view.status_code; request.response.content_type = "text/html"; return; @@ -234,8 +266,7 @@ fn renderTemplateWithLayout( view: jetzig.views.View, route: *jetzig.views.Route, ) ![]const u8 { - try view.data.addConst("jetzig_view", view.data.string(route.view_name)); - try view.data.addConst("jetzig_action", view.data.string(@tagName(route.action))); + try addTemplateConstants(view, route); if (request.getLayout(route)) |layout_name| { // TODO: Allow user to configure layouts directory other than src/app/views/layouts/ @@ -251,6 +282,11 @@ fn renderTemplateWithLayout( } else return try template.render(view.data); } +fn addTemplateConstants(view: jetzig.views.View, route: *const jetzig.views.Route) !void { + try view.data.addConst("jetzig_view", view.data.string(route.view_name)); + try view.data.addConst("jetzig_action", view.data.string(@tagName(route.action))); +} + fn isBadRequest(err: anyerror) bool { return switch (err) { error.JetzigBodyParseError, error.JetzigQueryParseError => true, diff --git a/src/jetzig/markdown.zig b/src/jetzig/markdown.zig new file mode 100644 index 0000000..ce9f29c --- /dev/null +++ b/src/jetzig/markdown.zig @@ -0,0 +1,43 @@ +const std = @import("std"); + +const jetzig = @import("../jetzig.zig"); + +const Zmd = @import("zmd").Zmd; +pub fn render( + allocator: std.mem.Allocator, + route: *const jetzig.views.Route, + custom_fragments: ?type, +) !?[]const u8 { + const fragments = custom_fragments orelse jetzig.config.get(type, "markdown_fragments"); + + var path_buf = std.ArrayList([]const u8).init(allocator); + defer path_buf.deinit(); + + try path_buf.appendSlice(&[_][]const u8{ "src", "app", "views" }); + + var it = std.mem.splitScalar(u8, route.uri_path, '/'); + while (it.next()) |segment| { + try path_buf.append(segment); + } + try path_buf.append(@tagName(route.action)); + + const base_path = try std.fs.path.join(allocator, path_buf.items); + defer allocator.free(base_path); + + const full_path = try std.mem.concat(allocator, u8, &[_][]const u8{ base_path, ".md" }); + defer allocator.free(full_path); + + const stat = try std.fs.cwd().statFile(full_path); + const markdown_content = std.fs.cwd().readFileAlloc(allocator, full_path, stat.size) catch |err| { + switch (err) { + error.FileNotFound => return null, + else => return err, + } + }; + + var zmd = Zmd.init(allocator); + defer zmd.deinit(); + + try zmd.parse(markdown_content); + return try zmd.toHtml(fragments); +}