diff --git a/api/build/BuildRunner.cfc b/api/build/BuildRunner.cfc index 3ed00b8fb..66d164468 100644 --- a/api/build/BuildRunner.cfc +++ b/api/build/BuildRunner.cfc @@ -88,14 +88,19 @@ component accessors=true { arguments.builder.injectMethod = this.injectMethod; - arguments.builder.injectMethod( "renderLinks", function( required string text ){ - return new api.rendering.WikiLinksRenderer( docTree=variables.docTree ).renderLinks( text=arguments.text, builder=variables._builder ); + arguments.builder.injectMethod( "renderLinks", function( required string text, required struct args, boolean markdown=false ){ + return new api.rendering.WikiLinksRenderer( docTree=variables.docTree ).renderLinks( + text = arguments.text, + builder = variables._builder, + args = arguments.args, + markdown = arguments.markdown + ); } ); - arguments.builder.injectMethod( "renderTemplate", function( required string template, struct args={} ){ + arguments.builder.injectMethod( "renderTemplate", function( required string template, struct args={}, boolean markdown=false ){ var renderer = new api.rendering.TemplateRenderer(); - var rendered = renderer.render( argumentCollection=arguments, template=_rootPathForRenderer & arguments.template ); + var rendered = renderer.render( argumentCollection=arguments, template=_rootPathForRenderer & arguments.template, markdown=arguments.markdown ); - return builder.renderLinks( rendered ); + return builder.renderLinks( rendered, args, arguments.markdown ); } ); StructDelete( arguments.builder, "injectMethod" ); diff --git a/api/data/FunctionPage.cfc b/api/data/FunctionPage.cfc index c37c7b19f..97f99b1c1 100644 --- a/api/data/FunctionPage.cfc +++ b/api/data/FunctionPage.cfc @@ -17,13 +17,13 @@ component accessors=true extends="Page" { return super.getTitle() & "()"; } - public string function getUsageSignature() { + public string function getUsageSignature( boolean plainText=false ) { var usage = super.getTitle() & "("; var delim = " "; var optionalCount = 0; for( var argument in this.getArguments() ) { - if ( !argument.required ) { + if ( !argument.required && !arguments.plainText ) { usage &= ""; optionalCount++; } @@ -34,7 +34,9 @@ component accessors=true extends="Page" { } else { usage &= argument.type; } - usage &= ""; + if ( !argument.required && !arguments.plainText ) { + usage &= ""; + } delim = ", "; } //usage &= ""; diff --git a/api/data/MethodPage.cfc b/api/data/MethodPage.cfc index 6755ed394..8e6368fe5 100644 --- a/api/data/MethodPage.cfc +++ b/api/data/MethodPage.cfc @@ -16,13 +16,13 @@ component accessors=true extends="Page" { property name="member" type="struct"; property name="alias" type="string"; - public string function getUsageSignature() { + public string function getUsageSignature( boolean plainText=false ) { var usage = this.getMethodObject() & "." & this.getMethodName() & "("; var delim = " "; var optionalCount = 0; for( var argument in this.getArguments() ) { - if ( !argument.required ) { + if ( !argument.required && !arguments.plainText ) { usage &= ""; optionalCount++; } @@ -34,10 +34,14 @@ component accessors=true extends="Page" { } else { usage &= argument.type; } - usage &= ""; + if ( !argument.required && !arguments.plainText ) { + usage &= ""; + } delim = ", "; } - usage &= ""; + if ( !arguments.plainText ) { + usage &= ""; + } //usage &= RepeatString( " ]", optionalCount ); usage &= " )"; diff --git a/api/data/TagPage.cfc b/api/data/TagPage.cfc index 91d82cb13..93041fc5f 100644 --- a/api/data/TagPage.cfc +++ b/api/data/TagPage.cfc @@ -15,11 +15,11 @@ component accessors=true extends="Page" { property name="srcExtension" type="struct"; property name="introduced" type="string"; - public string function getUsageSignature() { + public string function getUsageSignature( boolean plainText=false ) { var newLine = Chr(10); var indent = RepeatString( " ", 4 ); var tagName = "cf" & LCase( this.getName() ); - var usage = "<" & tagName; + var usage = ( arguments.plainText ? "<" : "<" ) & tagName; var closingTag = ""; var bodyType = this.getBodyContentType(); var unnamedAttributes = ( this.getAttributeType() ?: "" ) == "noname"; @@ -30,7 +30,7 @@ component accessors=true extends="Page" { usage &= " ###attribute.type# #attribute.name###"; } else { usage &= newline & indent; - if ( !attribute.required ) { + if ( !attribute.required && !arguments.plainText ) { usage &= ""; } @@ -42,7 +42,7 @@ component accessors=true extends="Page" { usage &= attribute.type; } - if ( !attribute.required ) { + if ( !attribute.required && !arguments.plainText ) { usage &= ""; } } @@ -54,13 +54,21 @@ component accessors=true extends="Page" { switch( bodyType ) { case "free": - closingTag &= htmlEditFormat(">[#tagName#>]") + if ( arguments.plainText ) { + closingTag &= ">#tagName#>"; + } else { + closingTag &= htmlEditFormat(">[#tagName#>]"); + } break; case "required": - closingTag &= htmlEditFormat(">#tagName#>") + if ( arguments.plainText ) { + closingTag &= ">#tagName#>"; + } else { + closingTag &= htmlEditFormat(">#tagName#>"); + } break; default: - closingTag &= ">"; + closingTag &= ( arguments.plainText ? ">" : ">" ); break; } diff --git a/api/rendering/MarkdownSyntaxHighlighter.cfc b/api/rendering/MarkdownSyntaxHighlighter.cfc new file mode 100644 index 000000000..02684d99c --- /dev/null +++ b/api/rendering/MarkdownSyntaxHighlighter.cfc @@ -0,0 +1,69 @@ +component { + + public string function renderHighlights( required string text ) { + var rendered = arguments.text; + var highlight = ""; + var pos = 1; + + do { + highlight = _getNextHighlight( rendered, pos ); + if ( !IsNull( highlight ) ) { + rendered = Replace( rendered, highlight.rawMatch, renderHighlight( highlight.code, highlight.language ), "all" ); + pos = highlight.pos; + } + } while( !IsNull( highlight ) ); + + return rendered; + } + + public string function renderHighlight( required string code, required string language ) { + // Strip +trycf suffix for markdown + var cleanLanguage = arguments.language.reReplace( "\+trycf$", "" ); + + // Normalize language names for markdown + if ( cleanLanguage.reFindNoCase( "^(luceescript|cfs)" ) ) { + cleanLanguage = "javascript"; // or "js" depending on your markdown renderer + } else if ( cleanLanguage.reFindNoCase( "^(lucee|cfm|coldfusion)" ) ) { + cleanLanguage = "html"; // or "cfml" if supported + } else if ( cleanLanguage eq "yml" ) { + cleanLanguage = "yaml"; + } + + // Return plain markdown code block + return "```" & cleanLanguage & chr(10) & arguments.code & chr(10) & "```"; + } + +// PRIVATE HELPERS + private any function _getNextHighlight( required string text, required string startPos=1 ) { + var referenceRegex = "```([a-zA-Z\+]+)?\n(.*?)\n```"; + var match = ReFind( referenceRegex, arguments.text, arguments.startPos, true ); + var found = match.len[1] > 0; + var result = {}; + + if ( !found ) { + return; + } + + var precedingContent = match.pos[1] == 1 ? "" : Trim( Left( arguments.text, match.pos[1]-1 ) ); + var matchIsWithinCodeBlock = precedingContent.endsWith( "
" ) || precedingContent.endsWith( "" );
+
+ if ( matchIsWithinCodeBlock ) {
+ return _getNextHighlight( arguments.text, match.pos[1]+match.len[1] );
+ }
+
+ result = {
+ rawMatch = Mid( arguments.text, match.pos[1], match.len[1] )
+ , code = Mid( arguments.text, match.pos[3], match.len[3] )
+ , pos = match.len[3]
+ };
+
+ if ( match.pos[2] ) {
+ result.language = Mid( arguments.text, match.pos[2], match.len[2] );
+ } else {
+ result.language = "text";
+ }
+
+ return result;
+ }
+
+}
diff --git a/api/rendering/SyntaxHighlighter.cfc b/api/rendering/SyntaxHighlighter.cfc
index bfe77936b..457bb3047 100644
--- a/api/rendering/SyntaxHighlighter.cfc
+++ b/api/rendering/SyntaxHighlighter.cfc
@@ -1,6 +1,15 @@
component {
- public string function renderHighlights( required string text ) {
+ public string function renderHighlights( required string text, boolean markdown=false ) {
+ // For markdown mode, strip +trycf and normalize language names
+ if ( arguments.markdown ) {
+ var result = arguments.text.reReplace( "```([a-zA-Z]+)\+trycf\n", "```\1" & chr( 10 ), "all" );
+ // Replace luceescript with cfml for better LLM compatibility
+ result = result.reReplace( "```luceescript\n", "```cfml" & chr( 10 ), "all" );
+ result = result.reReplace( "```lucee\n", "```cfml" & chr( 10 ), "all" );
+ return result;
+ }
+
var rendered = arguments.text;
var highlight = "";
var pos = 1;
@@ -8,7 +17,7 @@ component {
do {
highlight = _getNextHighlight( rendered, pos );
if ( !IsNull( highlight ) ) {
- rendered = Replace( rendered, highlight.rawMatch, renderHighlight( highlight.code, highlight.language ), "all" );
+ rendered = Replace( rendered, highlight.rawMatch, renderHighlight( highlight.code, highlight.language, arguments.markdown ), "all" );
pos = highlight.pos;
}
} while( !IsNull( highlight ) );
@@ -16,7 +25,23 @@ component {
return rendered;
}
- public string function renderHighlight( required string code, required string language ) {
+ public string function renderHighlight( required string code, required string language, boolean markdown=false ) {
+ // If markdown mode, just clean up the code block and return it as markdown
+ if ( arguments.markdown ) {
+ var cleanLanguage = arguments.language.reReplace( "\+trycf$", "" );
+
+ // Normalize language names for markdown
+ if ( cleanLanguage.reFindNoCase( "^(luceescript|cfs)" ) ) {
+ cleanLanguage = "javascript";
+ } else if ( cleanLanguage.reFindNoCase( "^(lucee|cfm|coldfusion)" ) ) {
+ cleanLanguage = "html";
+ } else if ( cleanLanguage eq "yml" ) {
+ cleanLanguage = "yaml";
+ }
+
+ return "```" & cleanLanguage & chr( 10 ) & arguments.code & chr( 10 ) & "```";
+ }
+
var highlighter = new Pygments();
var useTryCf = reFind( "\+trycf$", arguments.language ) > 0;
diff --git a/api/rendering/TemplateRenderer.cfc b/api/rendering/TemplateRenderer.cfc
index 60983bac3..dd0e75b16 100644
--- a/api/rendering/TemplateRenderer.cfc
+++ b/api/rendering/TemplateRenderer.cfc
@@ -1,6 +1,6 @@
component {
- public string function render( required string template, struct args={}, string helpers="" ) {
+ public string function render( required string template, struct args={}, string helpers="", boolean markdown=false ) {
var rendered = "";
_includeHelpers( arguments.helpers );
@@ -9,7 +9,7 @@ component {
include template=arguments.template;
}
- rendered = new SyntaxHighlighter().renderHighlights( rendered );
+ rendered = new SyntaxHighlighter().renderHighlights( rendered, arguments.markdown );
return Trim( rendered );
}
diff --git a/api/rendering/WikiLinksRenderer.cfc b/api/rendering/WikiLinksRenderer.cfc
index d66455443..3e809a3ea 100644
--- a/api/rendering/WikiLinksRenderer.cfc
+++ b/api/rendering/WikiLinksRenderer.cfc
@@ -2,7 +2,7 @@ component accessors=true {
property name="docTree";
- public string function renderLinks( required string text, required any builder ) {
+ public string function renderLinks( required string text, required any builder, required struct args, boolean markdown=false ) {
var rendered = arguments.text;
var link = "";
var startPos = 1;
@@ -14,7 +14,8 @@ component accessors=true {
rendered = Replace( rendered, link.rawMatch, content, "one" );
//startPos = link.nextStartPos + len( content );
} else {
- rendered = Replace( rendered, link.rawMatch, arguments.builder.renderLink( link.page ?: NullValue(), link.title ), "all" );
+ rendered = Replace( rendered, link.rawMatch,
+ arguments.builder.renderLink( link.page ?: NullValue(), link.title, args, arguments.markdown ), "all" );
startPos = link.nextStartPos;
}
}
diff --git a/builders/html/Builder.cfc b/builders/html/Builder.cfc
index 097369a76..adc79dd0a 100644
--- a/builders/html/Builder.cfc
+++ b/builders/html/Builder.cfc
@@ -1,16 +1,4 @@
component {
- public string function renderLink( any page, required string title ) {
- if ( IsNull( arguments.page ) ) {
- if (arguments.title.left(4) eq "http"){
- return '#HtmlEditFormat( arguments.title )#';
- } else {
- request.logger (text="Missing docs link: [[#HtmlEditFormat( arguments.title )#]]", type="WARN");
- return '#HtmlEditFormat( arguments.title )#';
- }
- }
- var link = arguments.page.getPath() & ".html";
- return '#HtmlEditFormat( arguments.title )#';
- }
public string function renderContent( any docTree, required string content ) {
return docTree.renderContent( content );
@@ -32,6 +20,11 @@ component {
request.filesWritten = 0;
request.filesToWrite = StructCount(pagePaths);
+ // purge previous build directory contents
+ if ( directoryExists( buildDirectory ) )
+ DirectoryDelete( buildDirectory, true );
+ directoryCreate( buildDirectory );
+
request.logger (text="Builder HTML directory: #arguments.buildDirectory#");
new api.parsers.ParserFactory().getMarkdownParser(); // so the parser in use shows up in logs
@@ -39,10 +32,20 @@ component {
//for ( var path in pagePaths ) {
each(pagePaths, function(path){
var tick = getTickCount();
- if (pagePaths[arguments.path].page.isPage())
- _writePage( pagePaths[arguments.path].page, buildDirectory, docTree );
+ var page = pagePaths[ arguments.path ].page;
+
+ if ( page.isPage() ) {
+ // write out full html page
+ var pageContent = renderPageContent( page, docTree, false, {} );
+ _writePage( page, buildDirectory, docTree, pageContent, {} );
+
+ // write out markdown page
+ var markdownContent = renderMarkdownContent( page, docTree );
+ _writeMarkdownPage( page, buildDirectory, markdownContent, docTree );
+
+ request.filesWritten++;
+ }
- request.filesWritten++;
if ((request.filesWritten mod 100) eq 0){
request.logger(text="Rendering Documentation (#request.filesWritten# / #request.filesToWrite#)");
}
@@ -58,12 +61,34 @@ component {
_writeSearchIndex( arguments.docTree, arguments.buildDirectory );
}
- public string function renderPage( required any page, required any docTree, required boolean edit ){
+ public string function renderPageContent( required any page, required any docTree,
+ required boolean edit, required struct htmlOpts ){
+ try {
+ var contentArgs = { page = arguments.page, docTree=arguments.docTree, edit=arguments.edit, htmlOpts=arguments.htmlOpts };
+
+ var pageContent = renderTemplate(
+ template = "templates/#_getPageLayoutFile( arguments.page )#.cfm"
+ , args = contentArgs
+ , helpers = "/builders/html/helpers"
+ );
+ } catch( any e ) {
+ e.additional.luceeDocsTitle = arguments.page.getTitle();
+ e.additional.luceeDocsPath = arguments.page.getPath();
+ e.additional.luceeDocsPageId = arguments.page.getid();
+ rethrow;
+ }
+ return pageContent;
+ }
+
+ public string function renderMarkdownContent( required any page, required any docTree ){
try {
- var renderedPage = renderTemplate(
- template = "templates/#_getPageLayoutFile( arguments.page )#.cfm"
- , args = { page = arguments.page, docTree=arguments.docTree, edit=arguments.edit }
+ var contentArgs = { page = arguments.page, docTree=arguments.docTree, edit=false };
+
+ var markdownContent = renderTemplate(
+ template = "../markdown/templates/#_getPageLayoutFile( arguments.page )#.cfm"
+ , args = contentArgs
, helpers = "/builders/html/helpers"
+ , markdown = true
);
} catch( any e ) {
e.additional.luceeDocsTitle = arguments.page.getTitle();
@@ -71,7 +96,13 @@ component {
e.additional.luceeDocsPageId = arguments.page.getid();
rethrow;
}
- var crumbs = arguments.docTree.getPageBreadCrumbs(arguments.page);
+ return markdownContent;
+ }
+
+ public string function renderPage( required any page, required any docTree,
+ required string pageContent, required boolean edit, required struct htmlOptions ){
+
+ var crumbs = arguments.docTree.getPageBreadCrumbs( arguments.page );
var excludeLinkMap = {}; // tracks links to exclude from See also
var links = [];
var categories = [];
@@ -117,21 +148,50 @@ component {
break;
}
}
+
+ var template = arguments.htmlOptions.mainTemplate ?: "main.cfm";
+ var crumbsArgs = {
+ crumbs:crumbs,
+ page: arguments.page,
+ docTree: arguments.docTree,
+ categories: categories.sort("textNoCase"),
+ edit: arguments.edit,
+ htmlOpts: arguments.htmlOptions
+ };
+ var seeAlsoArgs = {
+ links= links,
+ htmlOpts=arguments.htmlOptions
+ }
+
try {
+
+ var args = {
+ body = Trim( arguments.pageContent )
+ , htmlOpts = arguments.htmlOptions
+ , page = arguments.page
+ , edit = arguments.edit
+ , crumbs = renderTemplate( template="layouts/breadcrumbs.cfm", helpers = "/builders/html/helpers",
+ args = crumbsArgs
+ )
+ , seeAlso = renderTemplate( template="layouts/seeAlso.cfm" , helpers = "/builders/html/helpers",
+ args = seeAlsoArgs )
+ };
+
+ if ( !structKeyExists(arguments.htmlOptions, "no_navigation" ) ){
+ args.navTree = renderTemplate( template="layouts/sideNavTree.cfm", helpers = "/builders/html/helpers", args={
+ crumbs=crumbs,
+ docTree=arguments.docTree,
+ pageLineage=arguments.page.getLineage(),
+ pageLineageMap=arguments.page.getPageLineageMap()
+ } );
+ } else {
+ args.navTree = "";
+ }
+
var pageContent = renderTemplate(
- template = "layouts/main.cfm"
+ template = "layouts/#template#"
, helpers = "/builders/html/helpers"
- , args = {
- body = Trim( renderedPage )
- , page = arguments.page
- , edit = arguments.edit
- , crumbs = renderTemplate( template="layouts/breadcrumbs.cfm", helpers = "/builders/html/helpers", args={ crumbs=crumbs, page=arguments.page, docTree=arguments.docTree, categories=categories.sort("textNoCase"), edit= arguments.edit } )
- , navTree = renderTemplate( template="layouts/sideNavTree.cfm", helpers = "/builders/html/helpers", args={
- crumbs=crumbs, docTree=arguments.docTree, pageLineage=arguments.page.getLineage(), pageLineageMap=arguments.page.getPageLineageMap()
- } )
- , seeAlso = renderTemplate( template="layouts/seeAlso.cfm" , helpers = "/builders/html/helpers",
- args={ links=links } )
- }
+ , args = args
);
} catch( any e ) {
//e.additional.luceeDocsPage = arguments.page;
@@ -178,18 +238,24 @@ component {
}
// PRIVATE HELPERS
- private void function _writePage( required any page, required string buildDirectory, required any docTree ) {
+ private void function _writePage( required any page, required string buildDirectory,
+ required any docTree, required string pageContent, required struct htmlOptions ) {
var filePath = variables._getHtmlFilePath( arguments.page, arguments.buildDirectory );
var fileDirectory = GetDirectoryFromPath( filePath );
- //var starttime = getTickCount();
lock name="CreateDirectory" timeout=10 {
if ( !DirectoryExists( fileDirectory ) ) {
DirectoryCreate( fileDirectory );
}
}
- var pageContent = variables.cleanHtml(variables.renderPage( arguments.page, arguments.docTree, false ));
- FileWrite( filePath, pageContent );
+
+ var html = variables.cleanHtml(
+ variables.renderPage( arguments.page,
+ arguments.docTree, arguments.pageContent, false ,
+ arguments.htmlOptions, arguments.buildDirectory
+ )
+ );
+ FileWrite( filePath, html );
}
// regex strips left over whitespace multiple new lines
@@ -206,6 +272,35 @@ component {
return arguments.buildDirectory & arguments.page.getPath() & ".html";
}
+ private void function _writeMarkdownPage( required any page, required string buildDirectory, required string markdownContent, required any docTree ) {
+ var filePath = _getMarkdownFilePath( arguments.page, arguments.buildDirectory );
+ var fileDirectory = GetDirectoryFromPath( filePath );
+
+ lock name="CreateDirectory" timeout=10 {
+ if ( !DirectoryExists( fileDirectory ) ) {
+ DirectoryCreate( fileDirectory );
+ }
+ }
+
+ FileWrite( filePath, arguments.markdownContent );
+
+ // Set last modified date from git for recipes
+ var recipeDates = arguments.docTree.getRecipeDates();
+ var pageId = arguments.page.getId();
+ if ( structKeyExists( recipeDates, pageId ) ) {
+ FileSetLastModified( filePath, recipeDates[pageId] );
+ }
+ }
+
+ private string function _getMarkdownFilePath( required any page, required string buildDirectory ) {
+ if ( arguments.page.getPath() == "/home" ) {
+ return arguments.buildDirectory & "/index.md";
+ }
+
+ return arguments.buildDirectory & arguments.page.getPath() & ".md";
+ }
+
+
private void function _copyStaticAssets( required string buildDirectory ) {
updateHighlightsCss( arguments.buildDirectory );
var subdirs = directoryList(path=GetDirectoryFromPath( GetCurrentTemplatePath() ) & "/assets", type="dir", recurse="false");
@@ -219,8 +314,10 @@ component {
private function updateHighlightsCss( required string buildDirectory ){
var highlighter = new api.rendering.Pygments();
- var cssFile = path=GetDirectoryFromPath( GetCurrentTemplatePath() ) & "/assets/css/highlight.css";
- fileWrite( cssFile, highlighter.getCss() );
+ var cssFile = GetDirectoryFromPath( GetCurrentTemplatePath() ) & "/assets/css/highlight.css";
+ var css = highlighter.getCss();
+ if ( trim( css ) neq trim( fileRead( cssFile ) ) )
+ fileWrite( cssFile, highlighter.getCss() ); // only update if changed
}
private void function _copySiteImages( required string buildDirectory, required any docTree ) {
@@ -310,4 +407,104 @@ component {
return '#chr(10)#' &
ArrayToList(siteMap, chr(10) ) & '#chr(10)# ';
}
+
+ public string function renderLink( any page, required string title, struct args={}, boolean markdown=false ) {
+ if ( IsNull( arguments.page ) ) {
+ if ( arguments.title.left( 4 ) eq "http" ){
+ if ( arguments.markdown ) {
+ return "[#arguments.title#](#arguments.title#)";
+ }
+ return '#HtmlEditFormat( arguments.title )#';
+ } else {
+ request.logger( text="Missing docs link: [[#HtmlEditFormat( arguments.title )#]]", type="WARN" );
+ if ( arguments.markdown ) {
+ return "**#arguments.title#**";
+ }
+ return '#HtmlEditFormat( arguments.title )#';
+ }
+ }
+
+ var extension = arguments.markdown ? ".md" : ".html";
+ var link = arguments.page.getPath() & extension;
+
+ // For markdown, use relative paths
+ if ( arguments.markdown && structKeyExists( arguments.args, "page" ) ) {
+ link = _calculateRelativePath( arguments.args.page.getPath(), arguments.page.getPath() ) & extension;
+ } else if ( structKeyExists( arguments.args, "htmlOpts" )
+ && structKeyExists( arguments.args.htmlOpts, "base_href" ) ){
+ link = arguments.args.htmlOpts.base_href & link;
+ }
+
+ if ( arguments.markdown ) {
+ return "[#arguments.title#](#link#)";
+ }
+ return '#HtmlEditFormat( arguments.title )#';
+ }
+
+ private string function _calculateRelativePath( required string fromPath, required string toPath ) {
+ // Handle home page specially
+ if ( arguments.fromPath == "/home" ) {
+ arguments.fromPath = "/";
+ }
+ if ( arguments.toPath == "/home" ) {
+ arguments.toPath = "/";
+ }
+
+ // Split paths into parts (excluding filename)
+ var fromParts = listToArray( arguments.fromPath, "/" );
+ var toParts = listToArray( arguments.toPath, "/" );
+
+ // If they're in the same directory, just use the filename
+ if ( arrayLen( fromParts ) == arrayLen( toParts ) ) {
+ var samePath = true;
+ for ( var i = 1; i < arrayLen( fromParts ); i++ ) {
+ if ( fromParts[i] != toParts[i] ) {
+ samePath = false;
+ break;
+ }
+ }
+ if ( samePath ) {
+ return listLast( arguments.toPath, "/" );
+ }
+ }
+
+ // Find common prefix
+ var commonLength = 0;
+ var minLength = min( arrayLen( fromParts ) - 1, arrayLen( toParts ) - 1 );
+ for ( var i = 1; i <= minLength; i++ ) {
+ if ( fromParts[i] == toParts[i] ) {
+ commonLength = i;
+ } else {
+ break;
+ }
+ }
+
+ // Build relative path
+ var relativeParts = [];
+
+ // Go up from current location (exclude the filename itself)
+ var upLevels = arrayLen( fromParts ) - 1 - commonLength;
+ for ( var i = 1; i <= upLevels; i++ ) {
+ arrayAppend( relativeParts, ".." );
+ }
+
+ // Go down to target location
+ for ( var i = commonLength + 1; i <= arrayLen( toParts ); i++ ) {
+ arrayAppend( relativeParts, toParts[i] );
+ }
+
+ return arrayToList( relativeParts, "/" );
+ }
+
+ public string function _getIssueTrackerLink(required string name) {
+ var link = Replace( new api.build.BuildProperties().getIssueTrackerLink(), "{search}", urlEncodedFormat(arguments.name) )
+ return 'Search Issue Tracker ';
+ }
+
+ public string function _getTestCasesLink(required string name) {
+ var link = Replace( new api.build.BuildProperties().getTestCasesLink(), "{search}", urlEncodedFormat(arguments.name) )
+ return 'Search Lucee Test Cases (good for further, detailed examples)';
+ }
+
+
}
diff --git a/builders/html/helpers/categoriesAndRelated.cfm b/builders/html/helpers/categoriesAndRelated.cfm
new file mode 100644
index 000000000..4a9f0411d
--- /dev/null
+++ b/builders/html/helpers/categoriesAndRelated.cfm
@@ -0,0 +1,41 @@
+
+function renderCategoriesAndRelated( required any page, required any docTree, boolean markdown=false ) {
+ var categories = [];
+ var related = [];
+ var output = "";
+
+ if ( !IsNull( arguments.page.getCategories() ) ) {
+ for ( var cat in arguments.page.getCategories() ) {
+ var catId = "category-" & cat;
+ if ( arguments.docTree.pageExists( catId ) ) {
+ arrayAppend( categories, "[[#catId#]]" );
+ }
+ }
+ }
+
+ var excludeMap = {};
+ excludeMap[arguments.page.getId()] = "";
+ for ( var child in arguments.page.getChildren() ) {
+ excludeMap[child.getId()] = "";
+ }
+
+ var relatedIds = arguments.docTree.getPageRelated( arguments.page );
+ for ( var relId in relatedIds ) {
+ if ( len( relId ) gt 0 and not structKeyExists( excludeMap, relId ) ) {
+ arrayAppend( related, "[[#relId#]]" );
+ }
+ }
+
+ if ( arrayLen( categories ) ) {
+ output &= chr( 10 ) & "## Categories" & chr( 10 ) & chr( 10 );
+ output &= arrayToList( categories, ", " ) & chr( 10 );
+ }
+
+ if ( arrayLen( related ) ) {
+ output &= chr( 10 ) & "## See Also" & chr( 10 ) & chr( 10 );
+ output &= arrayToList( related, ", " ) & chr( 10 );
+ }
+
+ return output;
+}
+
diff --git a/builders/html/helpers/markdownTableCell.cfm b/builders/html/helpers/markdownTableCell.cfm
new file mode 100644
index 000000000..b0dd2a54c
--- /dev/null
+++ b/builders/html/helpers/markdownTableCell.cfm
@@ -0,0 +1,14 @@
+
+function markdownTableCell( required string content ) {
+ var cleaned = arguments.content;
+ // Remove newlines and replace with spaces
+ cleaned = reReplace( cleaned, "\r?\n", " ", "all" );
+ // Remove pipes that would break the table
+ cleaned = replace( cleaned, "|", "\|", "all" );
+ // Collapse multiple spaces
+ cleaned = reReplace( cleaned, "\s+", " ", "all" );
+ // Trim
+ cleaned = trim( cleaned );
+ return cleaned;
+}
+
diff --git a/builders/html/layouts/breadcrumbs.cfm b/builders/html/layouts/breadcrumbs.cfm
index 263700897..34af6a36c 100644
--- a/builders/html/layouts/breadcrumbs.cfm
+++ b/builders/html/layouts/breadcrumbs.cfm
@@ -3,14 +3,17 @@
-
-
+
+
+
+
+
-
diff --git a/builders/html/layouts/html_head.cfm b/builders/html/layouts/html_head.cfm
new file mode 100644
index 000000000..f5ef336b2
--- /dev/null
+++ b/builders/html/layouts/html_head.cfm
@@ -0,0 +1,69 @@
+
+ local.baseHref = ( repeatString( '../', args.page.getDepth()-1 ) );
+ // this breaks the /static/ local server mode
+ //if (baseHref eq "")
+ // baseHref = "/";
+ local.path = args.page.getPath();
+ if ( local.path eq "/home" )
+ local.path = "/index";
+ local.pageHref = "https://docs.lucee.org#local.path#.html";
+ local.pagePath = "#local.path#.html";
+ local.pagePathMd = "#local.path#.md";
+ if (args.page.getTitle() neq "Lucee Documentation")
+ local.pageTitle = args.page.getTitle() & " :: Lucee Documentation";
+ else
+ local.pageTitle = args.page.getTitle();
+ // many sites (slack, discord one box etc) can't handle the escaped and strip out the tag name from previews
+ local.safePageTitle = EncodeForHtml( Replace( Replace( local.pageTitle, "<", "", "all" ), ">", "", "all" ) );
+ local.pageTitle = EncodeForHtml( local.pageTitle );
+ local.pageDescription = getMetaDescription( args.page, args.body );
+
+
+
+
+ #local.pageTitle#
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/builders/html/layouts/main.cfm b/builders/html/layouts/main.cfm
index 8c5585d1e..1eaeb8ebc 100644
--- a/builders/html/layouts/main.cfm
+++ b/builders/html/layouts/main.cfm
@@ -4,71 +4,13 @@
-
-
- local.baseHref = ( repeatString( '../', args.page.getDepth()-1 ) );
- // this breaks the /static/ local server mode
- //if (baseHref eq "")
- // baseHref = "/";
- local.path = args.page.getPath();
- if ( local.path eq "/home" )
- local.path = "/index";
- local.pageHref = "https://docs.lucee.org#local.path#.html";
- local.pagePath = "#local.path#.html";
- if (args.page.getTitle() neq "Lucee Documentation")
- local.pageTitle = args.page.getTitle() & " :: Lucee Documentation";
- else
- local.pageTitle = args.page.getTitle();
- // many sites (slack, discord one box etc) can't handle the escaped and strip out the tag name from previews
- local.safePageTitle = EncodeForHtml( Replace( Replace( local.pageTitle, "<", "", "all" ), ">", "", "all" ) );
- local.pageTitle = EncodeForHtml( local.pageTitle );
- local.pageDescription = getMetaDescription( args.page, args.body );
-
+
+
-
- #local.pageTitle#
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+