Index: src/blob.c ================================================================== --- src/blob.c +++ src/blob.c @@ -652,10 +652,64 @@ blob_extract(pFrom, i-pFrom->iCursor, pTo); while( iiCursor = i; return pTo->nUsed; } + +/* +** Extract a single token of one of the forms: +** +** ="TEXT" +** ='TEXT' +** =TEXT +** +** The leading = is present if and only if skipEq is true. +** +** TEXT is the part that is extracted. There can be whitespace on +** either side of the text. The TEXT ends at the matching delimiter, +** or at whitespace if there is no delimiter. +** +** Return true if an argument is found. Return zero and leave +** the cursor unchanged if there is no argument. +** +** The cursor of pFrom is left pointing at the first character past +** the end of the argument. +** +** pTo will be an ephermeral blob. If pFrom changes, it might alter +** pTo as well. +*/ +int blob_argument_token(Blob *pFrom, Blob *pTo, int skipEq){ + char *aData = pFrom->aData; + int n = pFrom->nUsed; + int i = pFrom->iCursor; + int iStart; + char cDelim; + while( i=n || aData[i]!='=' ) return 0; + i++; + while( i=n ) return 0; + cDelim = aData[i]; + if( cDelim=='\'' || cDelim=='"' ){ + if( i>=n-2 ) return 0; + i++; + iStart = pFrom->iCursor = i; + while( i=n ) return 0; + blob_extract(pFrom, i-iStart, pTo); + i++; + }else{ + iStart = pFrom->iCursor = i; + while( iiCursor = i; + return 1; +} /* ** Extract a single SQL token from pFrom and use it to initialize pTo. ** Return the number of bytes in the token. If no token is found, ** return 0. Index: src/markdown.c ================================================================== --- src/markdown.c +++ src/markdown.c @@ -372,11 +372,11 @@ if( data[0]!='<' ) return 0; i = (data[1]=='/') ? 2 : 1; if( (data[i]<'a' || data[i]>'z') && (data[i]<'A' || data[i]>'Z') ){ if( data[1]=='!' && size>=7 && data[2]=='-' && data[3]=='-' ){ for(i=6; ipLast ){ + pCtx->pLast->pNext = pNew; + if( pCtx->mnLevel>iLevel ) pCtx->mnLevel = iLevel; + }else{ + pCtx->mnLevel = iLevel; + } + pNew->pPrev = pCtx->pLast; + pCtx->pLast = pNew; + if( pCtx->pFirst==0 ) pCtx->pFirst = pNew; + pNew->zTitle = (char*)&pNew[1]; + memcpy(pNew->zTitle, zText, nText); + pNew->zTitle[nText] = 0; + pNew->zTag = pNew->zTitle + nText + 1; + pNew->iLevel = iLevel; + pNew->nth = 0; + + /* Generate an identifier. The identifer name is approximately the + ** same as a Pandoc identifier. + ** + ** * Skip all text up to the first letter. + ** * Remove all text past the last letter. + ** * Remove HTML markup and entities. + ** * Replace all whitespace sequences with a single "-" + ** * Remove all characters other than alphanumeric, "_", "-", and ".". + ** * Convert all alphabetics to lower case. + ** * If nothing remains, use "section" as the identifier. + */ + memcpy(pNew->zTag, zText, nText); + pNew->zTag[nText] = 0; + zTag = pNew->zTag; + for(i=j=0; zTag[i]; i++){ + char c = zTag[i]; + if( fossil_isupper(c) ){ + if( !seenChar ){ j = 0; seenChar = 1; } + zTag[j++] = fossil_tolower(c); + continue; + } + if( fossil_islower(c) ){ + if( !seenChar ){ j = 0; seenChar = 1; } + zTag[j++] = c; + continue; + } + if( c=='<' ){ + i += html_tag_length(zTag+i) - 1; + continue; + } + if( c=='&' ){ + while( zTag[i] && zTag[i]!=';' ){ i++; } + if( zTag[i]==0 ) break; + continue; + } + if( fossil_isspace(c) ){ + if( j && zTag[j-1]!='-' ) zTag[j++] = '-'; + while( fossil_isspace(zTag[i+1]) ){ i++; } + continue; + } + if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' ){ + if( j && zTag[j-1]!='-' ) zTag[j++] = '-'; + }else{ + zTag[j++] = c; + } + } + if( j==0 || !seenChar ){ + memcpy(zTag, "section", 7); + j = 7; + } + while( j>0 && !fossil_isalpha(zTag[j-1]) ){ j--; } + zTag[j] = 0; + + /* Search for duplicate identifiers and disambiguate */ + pNew->nth = 0; + for(pSearch=pNew->pPrev; pSearch; pSearch=pSearch->pPrev){ + if( strcmp(pSearch->zTag,zTag)==0 ){ + pNew->nth = pSearch->nth+1; + break; + } + } +} + /* INTER_BLOCK -- skip a line between block level elements */ #define INTER_BLOCK(ob) \ do { if( blob_size(ob)>0 ) blob_append_char(ob, '\n'); } while (0) @@ -137,15 +250,64 @@ static void html_epilog(struct Blob *ob, void *opaque){ INTER_BLOCK(ob); BLOB_APPEND_LITERAL(ob, "\n"); } + +/* +** If text is an HTML control comment, then deal with it and return true. +** Otherwise just return false without making any changes. +** +** We are looking for comments of the following form: +** +** +** +** +** +** In the paragraph-numbers=N form with N>1, N-th level headings are +** numbered like top-levels. N+1-th level headings are like 2nd levels. +** and so forth. +** +** In the toc=N form, a table of contents is generated for all headings +** less than or equal to leve N. +*/ +static int html_control_comment(Blob *ob, Blob *text, void *opaque){ + Blob token, arg; + MarkdownToHtml *pCtx; + if( blob_size(text)<20 ) return 0; + if( strncmp(blob_buffer(text),"",3) ){ + blob_appendf(ob, "", + blob_str(&token)); + } + blob_reset(&token); + } + return 1; +} static void html_blockhtml(struct Blob *ob, struct Blob *text, void *opaque){ char *data = blob_buffer(text); size_t size = blob_size(text); Blob *title = ((MarkdownToHtml*)opaque)->output_title; + if( html_control_comment(ob,text,opaque) ) return; while( size>0 && fossil_isspace(data[0]) ){ data++; size--; } while( size>0 && fossil_isspace(data[size-1]) ){ size--; } /* If the first raw block is an

element, then use it as the title. */ if( blob_size(ob)<=PROLOG_SIZE && size>9 @@ -180,19 +342,37 @@ struct Blob *ob, struct Blob *text, int level, void *opaque ){ - struct Blob *title = ((MarkdownToHtml*)opaque)->output_title; + MarkdownToHtml *pCtx = (MarkdownToHtml*)opaque; + MarkdownHeading *pHdng; + struct Blob *title = pCtx->output_title; /* The first header at the beginning of a text is considered as * a title and not output. */ if( blob_size(ob)<=PROLOG_SIZE && title!=0 && blob_size(title)==0 ){ BLOB_APPEND_BLOB(title, text); return; } INTER_BLOCK(ob); - blob_appendf(ob, "", level); + html_new_heading(pCtx, text, level); + pHdng = pCtx->pLast; + if( pHdng->nth ){ + blob_appendf(ob, "", level, pHdng->zTag, pHdng->nth); + }else{ + blob_appendf(ob, "", level, pHdng->zTag); + } + if( pCtx->iHdngNums && level>=pCtx->iHdngNums ){ + int i; + for(i=pCtx->iHdngNums-1; iaNum[i]); + } + blob_appendf(ob,"%d", ++pCtx->aNum[i]); + if( i==pCtx->iHdngNums-1 ) blob_append(ob, ".0", 2); + blob_append(ob, " ", 1); + for(i++; i<6; i++) pCtx->aNum[i] = 0; + } BLOB_APPEND_BLOB(ob, text); blob_appendf(ob, "", level); } static void html_hrule(struct Blob *ob, void *opaque){ @@ -303,16 +483,18 @@ BLOB_APPEND_LITERAL(ob, " \n"); BLOB_APPEND_BLOB(ob, cells); BLOB_APPEND_LITERAL(ob, " \n"); } - - /* HTML span tags */ - static int html_raw_html_tag(struct Blob *ob, struct Blob *text, void *opaque){ - blob_append(ob, blob_buffer(text), blob_size(text)); + if( html_control_comment(ob,text,opaque) ){ + /* No-op */ + }else{ + /* Everything else is passed through without change */ + blob_append(ob, blob_buffer(text), blob_size(text)); + } return 1; } static int html_autolink( struct Blob *ob, @@ -529,10 +711,53 @@ static void html_normal_text(struct Blob *ob, struct Blob *text, void *opaque){ html_escape(ob, blob_buffer(text), blob_size(text)); } + +/* +** Insert a table of contents into the body of the document. +** +** The pCtx provides the information needed to do this: +** +** pCtx->iToc Offset into pOut of where to insert the TOC +** pCtx->mxToc Maximum depth of the TOC +** pCtx->pFirst List of paragraphs to form the TOC +*/ +static void html_insert_toc(MarkdownToHtml *pCtx, Blob *pOut){ + Blob new; + MarkdownHeading *pX; + int iLevel = pCtx->mnLevel-1; + int iBase = iLevel; + blob_init(&new, 0, 0); + blob_append(&new, blob_buffer(pOut), pCtx->iToc); + blob_append(&new, "
\n", -1); + for(pX=pCtx->pFirst; pX; pX=pX->pNext){ + if( pX->iLevel>pCtx->mxToc ) continue; + while( iLeveliLevel ){ + iLevel++; + blob_appendf(&new, "
    \n", + iLevel - iBase); + } + while( iLevel>pX->iLevel ){ + iLevel--; + blob_appendf(&new, "
\n"); + } + blob_appendf(&new,"
  • ", pX->zTag); + html_to_plaintext(pX->zTitle, &new); + blob_appendf(&new,"
  • \n"); + } + while( iLevel>iBase ){ + iLevel--; + blob_appendf(&new, "\n"); + } + blob_appendf(&new, "
    \n"); + blob_append(&new, blob_buffer(pOut)+pCtx->iToc, + blob_size(pOut)-pCtx->iToc); + blob_reset(pOut); + *pOut = new; +} /* ** Convert markdown into HTML. ** ** The document title is placed in output_title if not NULL. Or if @@ -579,12 +804,19 @@ /* misc. parameters */ "*_", /* emph_chars */ 0 /* opaque */ }; MarkdownToHtml context; + MarkdownHeading *pHdng, *pNextHdng; + memset(&context, 0, sizeof(context)); context.output_title = output_title; html_renderer.opaque = &context; if( output_title ) blob_reset(output_title); blob_reset(output_body); markdown(output_body, input_markdown, &html_renderer); + if( context.mxToc>0 ) html_insert_toc(&context, output_body); + for(pHdng=context.pFirst; pHdng; pHdng=pNextHdng){ + pNextHdng = pHdng->pNext; + fossil_free(pHdng); + } }