summaryrefslogtreecommitdiff
path: root/src/org.c
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/org.c
downloadrelic-master.tar.gz
initial commitHEADmaster
Diffstat (limited to 'src/org.c')
-rw-r--r--src/org.c664
1 files changed, 664 insertions, 0 deletions
diff --git a/src/org.c b/src/org.c
new file mode 100644
index 0000000..a70d1ed
--- /dev/null
+++ b/src/org.c
@@ -0,0 +1,664 @@
+/*
+** Copyright (c) 2026, the Fossil SCM contributors
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the Simplified BSD License (also
+** known as the "2-Clause License" or "FreeBSD License".)
+**
+*******************************************************************************
+** A small, safe Org-mode renderer for embedded project documentation.
+** Executable Org features are intentionally unsupported.
+*/
+#include "config.h"
+#include "org.h"
+#include <assert.h>
+
+typedef struct OrgHeading OrgHeading;
+typedef struct OrgDoc OrgDoc;
+struct OrgHeading {
+ char *zText;
+ char *zId;
+ int level;
+};
+struct OrgDoc {
+ char *zCopy;
+ char **azLine;
+ int nLine;
+ int nAlloc;
+ OrgHeading *aHeading;
+ int nHeading;
+ int nHeadingAlloc;
+ char *zTitle;
+ char *zAuthor;
+ char *zDate;
+ int tocDepth;
+ int isWiki;
+};
+
+static char *org_strndup(const char *z, int n){
+ char *zOut = fossil_malloc((size_t)n + 1);
+ memcpy(zOut, z, (size_t)n);
+ zOut[n] = 0;
+ return zOut;
+}
+
+static char *org_trimdup(const char *z){
+ int n;
+ while( fossil_isspace(*z) ) z++;
+ n = (int)strlen(z);
+ while( n>0 && fossil_isspace(z[n-1]) ) n--;
+ return org_strndup(z, n);
+}
+
+static int org_starts_with(const char *z, const char *zPrefix){
+ return fossil_strnicmp(z, zPrefix, strlen(zPrefix))==0;
+}
+
+static const char *org_directive_value(const char *z, const char *zName){
+ int n = (int)strlen(zName);
+ if( z[0]!='#' || z[1]!='+' || fossil_strnicmp(z+2,zName,n)!=0
+ || z[n+2]!=':' ){
+ return 0;
+ }
+ z += n+3;
+ while( fossil_isspace(*z) ) z++;
+ return z;
+}
+
+static int org_heading(const char *z, int *pLevel, const char **pzText){
+ int n = 0;
+ while( z[n]=='*' ) n++;
+ if( n==0 || z[n]!=' ' ) return 0;
+ *pLevel = n>6 ? 6 : n;
+ while( z[n]==' ' ) n++;
+ *pzText = z+n;
+ return 1;
+}
+
+static char *org_slug(const char *z){
+ Blob out = BLOB_INITIALIZER;
+ int i;
+ int pendingDash = 0;
+ for(i=0; z[i]; i++){
+ unsigned char c = (unsigned char)z[i];
+ if( c>=0x80 || fossil_isalnum(c) ){
+ if( pendingDash && blob_size(&out)>0 ) blob_append_char(&out,'-');
+ blob_append_char(&out, c>=0x80 ? (char)c : (char)fossil_tolower(c));
+ pendingDash = 0;
+ }else if( c=='-' || fossil_isspace(c) ){
+ pendingDash = 1;
+ }
+ }
+ if( blob_size(&out)==0 ) blob_append(&out, "section", 7);
+ return blob_str(&out);
+}
+
+static int org_id_exists(OrgDoc *p, const char *zId){
+ int i;
+ for(i=0; i<p->nHeading; i++){
+ if( fossil_strcmp(p->aHeading[i].zId,zId)==0 ) return 1;
+ }
+ return 0;
+}
+
+static void org_add_heading(OrgDoc *p, int level, const char *zText){
+ OrgHeading *pH;
+ char *zBase = org_slug(zText);
+ char *zId = fossil_strdup(zBase);
+ int n = 2;
+ while( org_id_exists(p,zId) ){
+ fossil_free(zId);
+ zId = mprintf("%s-%d", zBase, n++);
+ }
+ fossil_free(zBase);
+ if( p->nHeading>=p->nHeadingAlloc ){
+ p->nHeadingAlloc = p->nHeadingAlloc*2 + 16;
+ p->aHeading = fossil_realloc(p->aHeading,
+ sizeof(p->aHeading[0])*(size_t)p->nHeadingAlloc);
+ }
+ pH = &p->aHeading[p->nHeading++];
+ pH->level = level;
+ pH->zText = org_trimdup(zText);
+ pH->zId = zId;
+}
+
+static void org_doc_init(OrgDoc *p, Blob *pIn, int isWiki){
+ int i;
+ int iStart = 0;
+ int n = blob_size(pIn);
+ memset(p, 0, sizeof(*p));
+ p->isWiki = isWiki;
+ p->zCopy = org_strndup(blob_buffer(pIn), n);
+ for(i=0; i<=n; i++){
+ int isCrlf = p->zCopy[i]=='\r' && p->zCopy[i+1]=='\n';
+ if( p->zCopy[i]=='\r' || p->zCopy[i]=='\n' || p->zCopy[i]==0 ){
+ const char *zText;
+ const char *zValue;
+ int level;
+ p->zCopy[i] = 0;
+ if( p->nLine>=p->nAlloc ){
+ p->nAlloc = p->nAlloc*2 + 32;
+ p->azLine = fossil_realloc(p->azLine,
+ sizeof(p->azLine[0])*(size_t)p->nAlloc);
+ }
+ p->azLine[p->nLine++] = p->zCopy+iStart;
+ zText = p->zCopy+iStart;
+ if( (zValue = org_directive_value(zText,"title"))!=0 ){
+ fossil_free(p->zTitle);
+ p->zTitle = org_trimdup(zValue);
+ }else if( (zValue = org_directive_value(zText,"author"))!=0 ){
+ fossil_free(p->zAuthor);
+ p->zAuthor = org_trimdup(zValue);
+ }else if( (zValue = org_directive_value(zText,"date"))!=0 ){
+ fossil_free(p->zDate);
+ p->zDate = org_trimdup(zValue);
+ }else if( (zValue = org_directive_value(zText,"options"))!=0 ){
+ const char *zToc = zValue;
+ while( *zToc ){
+ if( org_starts_with(zToc,"toc:") ){
+ zToc += 4;
+ if( fossil_isdigit(*zToc) ) p->tocDepth = atoi(zToc);
+ else if( fossil_tolower(*zToc)=='t' ) p->tocDepth = 6;
+ else p->tocDepth = 0;
+ break;
+ }
+ while( *zToc && !fossil_isspace(*zToc) ) zToc++;
+ while( fossil_isspace(*zToc) ) zToc++;
+ }
+ }else if( org_heading(zText,&level,&zText) ){
+ org_add_heading(p, level, zText);
+ }
+ if( isCrlf ){
+ p->zCopy[++i] = 0;
+ }
+ iStart = i+1;
+ }
+ }
+ if( p->tocDepth>6 ) p->tocDepth = 6;
+}
+
+static void org_doc_reset(OrgDoc *p){
+ int i;
+ for(i=0; i<p->nHeading; i++){
+ fossil_free(p->aHeading[i].zText);
+ fossil_free(p->aHeading[i].zId);
+ }
+ fossil_free(p->aHeading);
+ fossil_free(p->azLine);
+ fossil_free(p->zTitle);
+ fossil_free(p->zAuthor);
+ fossil_free(p->zDate);
+ fossil_free(p->zCopy);
+}
+
+static const char *org_heading_id(OrgDoc *p, const char *zText){
+ int i;
+ char *zTrim = org_trimdup(zText);
+ for(i=0; i<p->nHeading; i++){
+ if( fossil_stricmp(zTrim,p->aHeading[i].zText)==0 ){
+ fossil_free(zTrim);
+ return p->aHeading[i].zId;
+ }
+ }
+ fossil_free(zTrim);
+ return 0;
+}
+
+static int org_is_markup_boundary(char c){
+ return c==0 || fossil_isspace(c) || strchr(".,;:!?()[]{}'\"",c)!=0;
+}
+
+static int org_find_close(const char *z, int n, int i, char c){
+ int j;
+ for(j=i+1; j<n; j++){
+ if( z[j]==c && j>i+1 && !fossil_isspace(z[j-1])
+ && org_is_markup_boundary(j+1<n ? z[j+1] : 0) ) return j;
+ }
+ return 0;
+}
+
+static int org_safe_file_link(const char *z){
+ int i;
+ if( z[0]==0 || z[0]=='/' || z[0]=='\\' ) return 0;
+ for(i=0; z[i] && z[i]!='?' && z[i]!='#'; i++){
+ if( z[i]==':' || z[i]=='\\' ) return 0;
+ }
+ return 1;
+}
+
+/* Wiki pages have no source directory. Their file links are rooted at the
+** repository and may not use parent-directory traversal. */
+static int org_safe_wiki_file_link(const char *z){
+ const char *p = z;
+ if( !org_safe_file_link(z) ) return 0;
+ while( *p ){
+ const char *zStart = p;
+ while( *p && *p!='/' && *p!='?' && *p!='#' ) p++;
+ if( p-zStart==2 && zStart[0]=='.' && zStart[1]=='.' ) return 0;
+ if( *p!='/' ) break;
+ p++;
+ }
+ return 1;
+}
+
+static void org_inline(OrgDoc *p, Blob *pOut, const char *z, int n){
+ int i = 0;
+ if( n<0 ) n = (int)strlen(z);
+ while( i<n ){
+ if( i+1<n && z[i]=='[' && z[i+1]=='[' ){
+ int j;
+ for(j=i+2; j+1<n && !(z[j]==']' && z[j+1]==']'); j++){}
+ if( j+1<n ){
+ int k;
+ int nTarget;
+ int iLabel;
+ int nLabel;
+ const char *zHref = 0;
+ char *zOwned = 0;
+ for(k=i+2; k<j && !(z[k]==']' && z[k+1]=='['); k++){}
+ nTarget = k-(i+2);
+ iLabel = k<j ? k+2 : i+2;
+ nLabel = k<j ? j-iLabel : nTarget;
+ if( nTarget>1 && z[i+2]=='*' ){
+ char *zTarget = org_strndup(z+i+3,nTarget-1);
+ const char *zId = org_heading_id(p,zTarget);
+ if( zId ) zHref = zOwned = mprintf("#%s",zId);
+ fossil_free(zTarget);
+ }else if( nTarget>7 && fossil_strnicmp(z+i+2,"http://",7)==0 ){
+ zHref = zOwned = org_strndup(z+i+2,nTarget);
+ }else if( nTarget>8 && fossil_strnicmp(z+i+2,"https://",8)==0 ){
+ zHref = zOwned = org_strndup(z+i+2,nTarget);
+ }else if( p->isWiki && nTarget>5
+ && fossil_strnicmp(z+i+2,"wiki:",5)==0 ){
+ char *zTarget = org_strndup(z+i+7,nTarget-5);
+ char *zFragment = strchr(zTarget,'#');
+ char *zSlug = 0;
+ if( zFragment ){
+ *zFragment++ = 0;
+ zSlug = org_slug(zFragment);
+ }
+ if( zTarget[0] ){
+ zOwned = zSlug
+ ? mprintf("wiki?name=%T#%T",zTarget,zSlug)
+ : mprintf("wiki?name=%T",zTarget);
+ zHref = zOwned;
+ if( k==j ){
+ iLabel = i+7;
+ nLabel = nTarget-5;
+ if( zFragment ) nLabel -= (int)strlen(zFragment)+1;
+ }
+ }
+ fossil_free(zSlug);
+ fossil_free(zTarget);
+ }else if( nTarget>5 && fossil_strnicmp(z+i+2,"file:",5)==0 ){
+ char *zTarget = org_strndup(z+i+7,nTarget-5);
+ if( p->isWiki && org_safe_wiki_file_link(zTarget) ){
+ zHref = zOwned = mprintf("doc/tip/%T",zTarget);
+ fossil_free(zTarget);
+ }else if( !p->isWiki && org_safe_file_link(zTarget) ){
+ zHref = zOwned = zTarget;
+ }else{
+ fossil_free(zTarget);
+ }
+ }
+ if( zHref ){
+ blob_append(pOut,"<a href=\"",-1);
+ htmlize_to_blob(pOut,zHref,-1);
+ blob_append(pOut,"\">",-1);
+ org_inline(p,pOut,z+iLabel,nLabel);
+ blob_append(pOut,"</a>",-1);
+ }else{
+ org_inline(p,pOut,z+iLabel,nLabel);
+ }
+ fossil_free(zOwned);
+ i = j+2;
+ continue;
+ }
+ }
+ if( strchr("*/_+~=",z[i])!=0 && i+2<n && !fossil_isspace(z[i+1])
+ && (i==0 || org_is_markup_boundary(z[i-1])) ){
+ int j = org_find_close(z,n,i,z[i]);
+ if( j>0 ){
+ const char *zOpen = 0;
+ const char *zClose = 0;
+ switch( z[i] ){
+ case '*': zOpen="<strong>"; zClose="</strong>"; break;
+ case '/': zOpen="<em>"; zClose="</em>"; break;
+ case '_': zOpen="<u>"; zClose="</u>"; break;
+ case '+': zOpen="<del>"; zClose="</del>"; break;
+ default: zOpen="<code>"; zClose="</code>"; break;
+ }
+ blob_append(pOut,zOpen,-1);
+ if( z[i]=='~' || z[i]=='=' ) htmlize_to_blob(pOut,z+i+1,j-i-1);
+ else org_inline(p,pOut,z+i+1,j-i-1);
+ blob_append(pOut,zClose,-1);
+ i = j+1;
+ continue;
+ }
+ }
+ if( z[i]=='\\' && i+1<n ) i++;
+ htmlize_to_blob(pOut,z+i,1);
+ i++;
+ }
+}
+
+static int org_list_item(const char *z, int *pIndent, int *pOrdered,
+ const char **pzText){
+ int i = 0;
+ int j;
+ while( z[i]==' ' || z[i]=='\t' ) i += z[i]=='\t' ? 8 : 1;
+ j = i;
+ if( z[j]=='-' || z[j]=='+' || z[j]=='*' ){
+ if( z[j+1]!=' ' ) return 0;
+ *pOrdered = 0;
+ *pzText = z+j+2;
+ }else if( fossil_isdigit(z[j]) ){
+ while( fossil_isdigit(z[j]) ) j++;
+ if( (z[j]!='.' && z[j]!=')') || z[j+1]!=' ' ) return 0;
+ *pOrdered = 1;
+ *pzText = z+j+2;
+ }else{
+ return 0;
+ }
+ *pIndent = i;
+ return 1;
+}
+
+static void org_render_lists(OrgDoc *p, Blob *pOut, int *pi, int iEnd,
+ int indent){
+ int ordered;
+ int itemIndent;
+ const char *zText;
+ const char *zTag;
+ if( *pi>=iEnd || !org_list_item(p->azLine[*pi],&itemIndent,&ordered,&zText) ){
+ return;
+ }
+ zTag = ordered ? "ol" : "ul";
+ blob_appendf(pOut,"<%s>\n",zTag);
+ while( *pi<iEnd ){
+ int nextIndent;
+ int nextOrdered;
+ const char *zNext;
+ if( !org_list_item(p->azLine[*pi],&itemIndent,&ordered,&zText)
+ || itemIndent!=indent || fossil_strcmp(zTag,ordered ? "ol" : "ul")!=0 ){
+ break;
+ }
+ blob_append(pOut,"<li>",-1);
+ org_inline(p,pOut,zText,-1);
+ (*pi)++;
+ while( *pi<iEnd
+ && org_list_item(p->azLine[*pi],&nextIndent,&nextOrdered,&zNext)
+ && nextIndent>indent ){
+ org_render_lists(p,pOut,pi,iEnd,nextIndent);
+ }
+ blob_append(pOut,"</li>\n",-1);
+ }
+ blob_appendf(pOut,"</%s>\n",zTag);
+}
+
+static int org_is_table_line(const char *z){
+ while( fossil_isspace(*z) ) z++;
+ return z[0]=='|';
+}
+
+static int org_is_table_rule(const char *z){
+ int nDash = 0;
+ while( *z ){
+ if( *z=='-' ) nDash++;
+ else if( *z!='|' && *z!='+' && *z!=':' && !fossil_isspace(*z) ) return 0;
+ z++;
+ }
+ return nDash>0;
+}
+
+static void org_table_row(OrgDoc *p, Blob *pOut, const char *z,
+ const char *zCell){
+ while( fossil_isspace(*z) ) z++;
+ if( *z=='|' ) z++;
+ blob_append(pOut,"<tr>",-1);
+ while( *z ){
+ const char *a = z;
+ const char *b = strchr(a,'|');
+ const char *zNext;
+ if( b==0 ) b = a+strlen(a);
+ zNext = *b ? b+1 : b;
+ while( a<b && fossil_isspace(*a) ) a++;
+ while( b>a && fossil_isspace(b[-1]) ) b--;
+ blob_appendf(pOut,"<%s>",zCell);
+ org_inline(p,pOut,a,(int)(b-a));
+ blob_appendf(pOut,"</%s>",zCell);
+ z = zNext;
+ }
+ blob_append(pOut,"</tr>\n",-1);
+}
+
+static void org_render_table(OrgDoc *p, Blob *pOut, int *pi){
+ int i = *pi;
+ int iEnd = i;
+ int hasHead;
+ while( iEnd<p->nLine && org_is_table_line(p->azLine[iEnd]) ) iEnd++;
+ hasHead = i+1<iEnd && org_is_table_rule(p->azLine[i+1]);
+ blob_append(pOut,"<table class=\"org-table\">\n",-1);
+ if( hasHead ){
+ blob_append(pOut,"<thead>\n",-1);
+ org_table_row(p,pOut,p->azLine[i],"th");
+ blob_append(pOut,"</thead>\n<tbody>\n",-1);
+ i += 2;
+ }else{
+ blob_append(pOut,"<tbody>\n",-1);
+ }
+ for(; i<iEnd; i++){
+ if( !org_is_table_rule(p->azLine[i]) ){
+ org_table_row(p,pOut,p->azLine[i],"td");
+ }
+ }
+ blob_append(pOut,"</tbody>\n</table>\n",-1);
+ *pi = iEnd;
+}
+
+static void org_render_block(OrgDoc *p, Blob *pOut, int *pi,
+ const char *zKind, const char *zArgs){
+ int i = *pi + 1;
+ int iEnd = i;
+ int isSrc = fossil_stricmp(zKind,"src")==0;
+ int isExample = fossil_stricmp(zKind,"example")==0;
+ char *zLang = 0;
+ if( isSrc ){
+ int n = 0;
+ while( fossil_isspace(*zArgs) ) zArgs++;
+ while( fossil_isalnum(zArgs[n]) || strchr("_+-",zArgs[n])!=0 ) n++;
+ if( n>0 ) zLang = org_strndup(zArgs,n);
+ }
+ while( iEnd<p->nLine ){
+ const char *z = p->azLine[iEnd];
+ while( fossil_isspace(*z) ) z++;
+ if( org_starts_with(z,"#+end_")
+ && fossil_stricmp(z+6,zKind)==0 ) break;
+ iEnd++;
+ }
+ if( isSrc || isExample ){
+ blob_appendf(pOut,"<pre class=\"org-block%s%s\"><code%s%s%s>",
+ isSrc ? " org-src" : " org-example",
+ isSrc && zLang ? " has-language" : "",
+ isSrc && zLang ? " class=\"language-" : "",
+ isSrc && zLang ? zLang : "",
+ isSrc && zLang ? "\"" : "");
+ }else{
+ blob_append(pOut,"<pre class=\"org-block org-unknown-block\"><code>",-1);
+ }
+ for(i=*pi+1; i<iEnd; i++){
+ htmlize_to_blob(pOut,p->azLine[i],-1);
+ if( i+1<iEnd ) blob_append_char(pOut,'\n');
+ }
+ blob_append(pOut,"</code></pre>\n",-1);
+ fossil_free(zLang);
+ *pi = iEnd<p->nLine ? iEnd+1 : iEnd;
+}
+
+static void org_render_toc(OrgDoc *p, Blob *pOut){
+ int i;
+ if( p->tocDepth<=0 ) return;
+ blob_append(pOut,"<nav class=\"org-toc\" aria-label=\"Table of contents\">",-1);
+ blob_append(pOut,"<h2>Contents</h2>\n",-1);
+ blob_append(pOut,"<ol>\n",-1);
+ for(i=0; i<p->nHeading; i++){
+ OrgHeading *pH = &p->aHeading[i];
+ if( pH->level>p->tocDepth ) continue;
+ blob_appendf(pOut,"<li class=\"org-toc-level-%d\"><a href=\"#",pH->level);
+ htmlize_to_blob(pOut,pH->zId,-1);
+ blob_append(pOut,"\">",-1);
+ org_inline(p,pOut,pH->zText,-1);
+ blob_append(pOut,"</a></li>\n",-1);
+ }
+ blob_append(pOut,"</ol>\n</nav>\n",-1);
+}
+
+static void org_to_html_impl(
+ Blob *pIn,
+ Blob *pTitle,
+ Blob *pOut,
+ int isWiki
+){
+ OrgDoc doc;
+ int i = 0;
+ int iHeading = 0;
+ int inDrawer = 0;
+ org_doc_init(&doc,pIn,isWiki);
+ blob_append(pOut,"<div class=\"org-document\">\n",-1);
+ if( pTitle && doc.zTitle ) blob_append(pTitle,doc.zTitle,-1);
+ if( !pTitle && doc.zTitle ){
+ blob_append(pOut,"<h1 class=\"org-title\">",-1);
+ org_inline(&doc,pOut,doc.zTitle,-1);
+ blob_append(pOut,"</h1>\n",-1);
+ }
+ if( doc.zAuthor || doc.zDate ){
+ blob_append(pOut,"<dl class=\"org-metadata\">\n",-1);
+ if( doc.zAuthor ){
+ blob_append(pOut,"<dt>Author</dt><dd>",-1);
+ org_inline(&doc,pOut,doc.zAuthor,-1);
+ blob_append(pOut,"</dd>\n",-1);
+ }
+ if( doc.zDate ){
+ blob_append(pOut,"<dt>Date</dt><dd>",-1);
+ org_inline(&doc,pOut,doc.zDate,-1);
+ blob_append(pOut,"</dd>\n",-1);
+ }
+ blob_append(pOut,"</dl>\n",-1);
+ }
+ org_render_toc(&doc,pOut);
+ while( i<doc.nLine ){
+ const char *z = doc.azLine[i];
+ const char *zText;
+ int level;
+ int indent;
+ int ordered;
+ while( fossil_isspace(*z) ) z++;
+ if( inDrawer ){
+ if( fossil_stricmp(z,":END:")==0 ) inDrawer = 0;
+ i++;
+ continue;
+ }
+ if( fossil_stricmp(z,":PROPERTIES:")==0 ){
+ inDrawer = 1;
+ i++;
+ continue;
+ }
+ if( z[0]==0 ){ i++; continue; }
+ if( z[0]=='#' && z[1]=='+' ){
+ if( org_starts_with(z,"#+begin_") ){
+ const char *zKind = z+8;
+ int nKind = 0;
+ char *zKindCopy;
+ while( zKind[nKind] && !fossil_isspace(zKind[nKind]) ) nKind++;
+ zKindCopy = org_strndup(zKind,nKind);
+ org_render_block(&doc,pOut,&i,zKindCopy,zKind+nKind);
+ fossil_free(zKindCopy);
+ }else{
+ i++;
+ }
+ continue;
+ }
+ if( org_heading(z,&level,&zText) ){
+ OrgHeading *pH = &doc.aHeading[iHeading++];
+ blob_appendf(pOut,"<h%d id=\"",level);
+ htmlize_to_blob(pOut,pH->zId,-1);
+ blob_append(pOut,"\">",-1);
+ org_inline(&doc,pOut,zText,-1);
+ blob_appendf(pOut,"</h%d>\n",level);
+ i++;
+ continue;
+ }
+ if( org_is_table_line(z) ){
+ org_render_table(&doc,pOut,&i);
+ continue;
+ }
+ if( org_list_item(doc.azLine[i],&indent,&ordered,&zText) ){
+ org_render_lists(&doc,pOut,&i,doc.nLine,indent);
+ continue;
+ }
+ blob_append(pOut,"<p>",-1);
+ {
+ int nParaLine = 0;
+ while( i<doc.nLine ){
+ const char *zLine = doc.azLine[i];
+ const char *zDummy;
+ int nDummy;
+ int oDummy;
+ while( fossil_isspace(*zLine) ) zLine++;
+ if( zLine[0]==0 || (zLine[0]=='#' && zLine[1]=='+')
+ || org_heading(zLine,&nDummy,&zDummy) || org_is_table_line(zLine)
+ || org_list_item(doc.azLine[i],&nDummy,&oDummy,&zDummy) ) break;
+ if( nParaLine++ ) blob_append_char(pOut,' ');
+ org_inline(&doc,pOut,zLine,-1);
+ i++;
+ }
+ }
+ blob_append(pOut,"</p>\n",-1);
+ }
+ blob_append(pOut,"</div>\n",-1);
+ org_doc_reset(&doc);
+}
+
+/* Render the supported, non-executable Org subset as embedded documentation. */
+void org_to_html(Blob *pIn, Blob *pTitle, Blob *pOut){
+ org_to_html_impl(pIn,pTitle,pOut,0);
+}
+
+/* Render Org as a Fossil wiki page. The wiki page name is its title, so an
+** optional #+TITLE is parsed but deliberately not emitted a second time. */
+void org_to_html_wiki(Blob *pIn, Blob *pOut){
+ Blob ignoredTitle = BLOB_INITIALIZER;
+ org_to_html_impl(pIn,&ignoredTitle,pOut,1);
+ blob_reset(&ignoredTitle);
+}
+
+/*
+** COMMAND: test-org-render
+**
+** Usage: %fossil test-org-render FILE ?--text?
+**
+** Render FILE with the built-in Org renderer. With --text, emit the
+** cleaned plain text used by full-text search.
+*/
+void test_org_render(void){
+ Blob in = BLOB_INITIALIZER;
+ Blob out = BLOB_INITIALIZER;
+ Blob title = BLOB_INITIALIZER;
+ int asText = find_option("text",0,0)!=0;
+ verify_all_options();
+ if( g.argc!=3 ) usage("FILE ?--text?");
+ blob_read_from_file(&in,g.argv[2],ExtFILE);
+ org_to_html(&in,asText ? &title : 0,&out);
+ if( asText ){
+ Blob plain = BLOB_INITIALIZER;
+ if( blob_size(&title) ) blob_appendf(&plain,"%s\n",blob_str(&title));
+ html_to_plaintext(blob_str(&out),&plain,HTOT_TRIM);
+ fossil_print("%b\n",&plain);
+ blob_reset(&plain);
+ }else{
+ fossil_print("%b\n",&out);
+ }
+ blob_reset(&title);
+ blob_reset(&out);
+ blob_reset(&in);
+}