From 4f49b16079b27d7ce633619e4cddfdf2bffbc952 Mon Sep 17 00:00:00 2001 From: cashmere Date: Fri, 4 Sep 2026 09:05:24 +0200 Subject: initial commit --- src/org.c | 664 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 664 insertions(+) create mode 100644 src/org.c (limited to 'src/org.c') 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 + +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; inHeading; 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; inHeading; 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; inHeading; 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; ji+1 && !fossil_isspace(z[j-1]) + && org_is_markup_boundary(j+11 && 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,"",-1); + org_inline(p,pOut,z+iLabel,nLabel); + blob_append(pOut,"",-1); + }else{ + org_inline(p,pOut,z+iLabel,nLabel); + } + fossil_free(zOwned); + i = j+2; + continue; + } + } + if( strchr("*/_+~=",z[i])!=0 && i+20 ){ + const char *zOpen = 0; + const char *zClose = 0; + switch( z[i] ){ + case '*': zOpen=""; zClose=""; break; + case '/': zOpen=""; zClose=""; break; + case '_': zOpen=""; zClose=""; break; + case '+': zOpen=""; zClose=""; break; + default: zOpen=""; zClose=""; 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=iEnd || !org_list_item(p->azLine[*pi],&itemIndent,&ordered,&zText) ){ + return; + } + zTag = ordered ? "ol" : "ul"; + blob_appendf(pOut,"<%s>\n",zTag); + while( *piazLine[*pi],&itemIndent,&ordered,&zText) + || itemIndent!=indent || fossil_strcmp(zTag,ordered ? "ol" : "ul")!=0 ){ + break; + } + blob_append(pOut,"
  • ",-1); + org_inline(p,pOut,zText,-1); + (*pi)++; + while( *piazLine[*pi],&nextIndent,&nextOrdered,&zNext) + && nextIndent>indent ){ + org_render_lists(p,pOut,pi,iEnd,nextIndent); + } + blob_append(pOut,"
  • \n",-1); + } + blob_appendf(pOut,"\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,"",-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( aa && fossil_isspace(b[-1]) ) b--; + blob_appendf(pOut,"<%s>",zCell); + org_inline(p,pOut,a,(int)(b-a)); + blob_appendf(pOut,"",zCell); + z = zNext; + } + blob_append(pOut,"\n",-1); +} + +static void org_render_table(OrgDoc *p, Blob *pOut, int *pi){ + int i = *pi; + int iEnd = i; + int hasHead; + while( iEndnLine && org_is_table_line(p->azLine[iEnd]) ) iEnd++; + hasHead = i+1azLine[i+1]); + blob_append(pOut,"\n",-1); + if( hasHead ){ + blob_append(pOut,"\n",-1); + org_table_row(p,pOut,p->azLine[i],"th"); + blob_append(pOut,"\n\n",-1); + i += 2; + }else{ + blob_append(pOut,"\n",-1); + } + for(; iazLine[i]) ){ + org_table_row(p,pOut,p->azLine[i],"td"); + } + } + blob_append(pOut,"\n
    \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( iEndnLine ){ + 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,"
    ",
    +      isSrc ? " org-src" : " org-example",
    +      isSrc && zLang ? " has-language" : "",
    +      isSrc && zLang ? " class=\"language-" : "",
    +      isSrc && zLang ? zLang : "",
    +      isSrc && zLang ? "\"" : "");
    +  }else{
    +    blob_append(pOut,"
    ",-1);
    +  }
    +  for(i=*pi+1; iazLine[i],-1);
    +    if( i+1
    \n",-1); + fossil_free(zLang); + *pi = iEndnLine ? iEnd+1 : iEnd; +} + +static void org_render_toc(OrgDoc *p, Blob *pOut){ + int i; + if( p->tocDepth<=0 ) return; + blob_append(pOut,"\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,"
    \n",-1); + if( pTitle && doc.zTitle ) blob_append(pTitle,doc.zTitle,-1); + if( !pTitle && doc.zTitle ){ + blob_append(pOut,"

    ",-1); + org_inline(&doc,pOut,doc.zTitle,-1); + blob_append(pOut,"

    \n",-1); + } + if( doc.zAuthor || doc.zDate ){ + blob_append(pOut,"
    \n",-1); + if( doc.zAuthor ){ + blob_append(pOut,"
    Author
    ",-1); + org_inline(&doc,pOut,doc.zAuthor,-1); + blob_append(pOut,"
    \n",-1); + } + if( doc.zDate ){ + blob_append(pOut,"
    Date
    ",-1); + org_inline(&doc,pOut,doc.zDate,-1); + blob_append(pOut,"
    \n",-1); + } + blob_append(pOut,"
    \n",-1); + } + org_render_toc(&doc,pOut); + while( izId,-1); + blob_append(pOut,"\">",-1); + org_inline(&doc,pOut,zText,-1); + blob_appendf(pOut,"\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,"

    ",-1); + { + int nParaLine = 0; + while( i\n",-1); + } + blob_append(pOut,"

    \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); +} -- cgit v1.3.1