Fossil SCM

Automatic table-of-contents generated for Markdown if there is a tag of the form: <!--markdown: toc=N --> where N is an integer that is the deepest level of content that will be added to the index. The TOC is inserted in place of the magic HTML comment.

drh 2020-09-15 19:50 auto-toc
Commit 6142e11d202d9a39c5916a7d7b00ed464fe8abeffa0f5c465c86aa9760b41eea
1 file changed +149 -2
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -38,26 +38,122 @@
3838
typedef struct MarkdownHeading MarkdownHeading;
3939
struct MarkdownHeading {
4040
MarkdownHeading *pPrev, *pNext; /* List of them all */
4141
char *zTitle; /* Text as displayed */
4242
char *zTag; /* Pandoc-style tag */
43
+ int iLevel; /* Level number for this entry */
44
+ int nth; /* This is the nth with the same tag */
4345
};
4446
4547
/*
4648
** An instance of the following structure is passed through the
4749
** "opaque" pointer.
4850
*/
4951
typedef struct MarkdownToHtml MarkdownToHtml;
5052
struct MarkdownToHtml {
5153
Blob *output_title; /* Store the title here */
52
- MarkdownHeading *pFirst, *pList; /* List of all headings */
54
+ MarkdownHeading *pFirst, *pLast; /* List of all headings */
5355
int iToc; /* Where to insert table-of-contents */
5456
int mxToc; /* Maximum table-of-content level */
57
+ int mnLevel; /* Minimum level seen over all headings */
5558
int iHdngNums; /* True to automatically number headings */
5659
int aNum[6]; /* Most recent number at each level */
5760
};
5861
62
+/*
63
+** Add a new heading to the heading list. This involves generating
64
+** a Pandoc-compatible identifier based on the heading text.
65
+*/
66
+static void html_new_heading(MarkdownToHtml *pCtx, Blob *text, int iLevel){
67
+ MarkdownHeading *pNew, *pSearch;
68
+ int nText = blob_size(text);
69
+ size_t n = sizeof(*pNew) + nText*2 + 10;
70
+ const char *zText = blob_buffer(text);
71
+ char *zTag;
72
+ int i, j;
73
+ int seenChar = 0;
74
+
75
+ pNew = fossil_malloc( n );
76
+ memset(pNew, 0, n);
77
+ if( pCtx->pLast ){
78
+ pCtx->pLast->pNext = pNew;
79
+ if( pCtx->mnLevel>iLevel ) pCtx->mnLevel = iLevel;
80
+ }else{
81
+ pCtx->mnLevel = iLevel;
82
+ }
83
+ pNew->pPrev = pCtx->pLast;
84
+ pCtx->pLast = pNew;
85
+ if( pCtx->pFirst==0 ) pCtx->pFirst = pNew;
86
+ pNew->zTitle = (char*)&pNew[1];
87
+ memcpy(pNew->zTitle, zText, nText);
88
+ pNew->zTitle[nText] = 0;
89
+ pNew->zTag = pNew->zTitle + nText + 1;
90
+ pNew->iLevel = iLevel;
91
+ pNew->nth = 0;
92
+
93
+ /* Generate an identifier. The identifer name is approximately the
94
+ ** same as a Pandoc identifier.
95
+ **
96
+ ** * Skip all text up to the first letter.
97
+ ** * Remove all text past the last letter.
98
+ ** * Remove HTML markup and entities.
99
+ ** * Replace all whitespace sequences with a single "-"
100
+ ** * Remove all characters other than alphanumeric, "_", "-", and ".".
101
+ ** * Convert all alphabetics to lower case.
102
+ ** * If nothing remains, use "section" as the identifier.
103
+ */
104
+ while( nText>0 && !fossil_isalpha(zText[nText-1]) ){ nText--; }
105
+ memcpy(pNew->zTag, zText, nText);
106
+ pNew->zTag[nText] = 0;
107
+ zTag = pNew->zTag;
108
+ for(i=j=0; zTag[i]; i++){
109
+ if( fossil_isupper(zTag[i]) ){
110
+ if( !seenChar ){ j = 0; seenChar = 1; }
111
+ zTag[j++] = fossil_tolower(zTag[i]);
112
+ continue;
113
+ }
114
+ if( fossil_islower(zTag[i]) ){
115
+ if( !seenChar ){ j = 0; seenChar = 1; }
116
+ zTag[j++] = zTag[i];
117
+ continue;
118
+ }
119
+ if( zTag[i]=='<' ){
120
+ i += html_tag_length(zTag+i) - 1;
121
+ continue;
122
+ }
123
+ if( zTag[i]=='&' ){
124
+ while( zTag[i] && zTag[i]!=';' ){ i++; }
125
+ if( zTag[i]==0 ) break;
126
+ continue;
127
+ }
128
+ if( fossil_isspace(zTag[i]) ){
129
+ zTag[j++] = '-';
130
+ while( fossil_isspace(zTag[i+1]) ){ i++; }
131
+ continue;
132
+ }
133
+ if( !fossil_isalnum(zTag[i]) && zTag[i]!='.' && zTag[i]!='_' ){
134
+ zTag[j++] = '-';
135
+ }else{
136
+ zTag[j++] = zTag[i];
137
+ }
138
+ }
139
+ if( j==0 || !seenChar ){
140
+ memcpy(zTag, "section", 7);
141
+ j = 7;
142
+ }
143
+ while( j>0 && !fossil_isalpha(zTag[j-1]) ){ j--; }
144
+ zTag[j] = 0;
145
+
146
+ /* Search for duplicate identifiers and disambiguate */
147
+ pNew->nth = 0;
148
+ for(pSearch=pNew->pPrev; pSearch; pSearch=pSearch->pPrev){
149
+ if( strcmp(pSearch->zTag,zTag)==0 ){
150
+ pNew->nth = pSearch->nth+1;
151
+ }
152
+ }
153
+}
154
+
59155
60156
/* INTER_BLOCK -- skip a line between block level elements */
61157
#define INTER_BLOCK(ob) \
62158
do { if( blob_size(ob)>0 ) blob_append_char(ob, '\n'); } while (0)
63159
@@ -246,19 +342,26 @@
246342
struct Blob *text,
247343
int level,
248344
void *opaque
249345
){
250346
MarkdownToHtml *pCtx = (MarkdownToHtml*)opaque;
347
+ MarkdownHeading *pHdng;
251348
struct Blob *title = pCtx->output_title;
252349
/* The first header at the beginning of a text is considered as
253350
* a title and not output. */
254351
if( blob_size(ob)<=PROLOG_SIZE && title!=0 && blob_size(title)==0 ){
255352
BLOB_APPEND_BLOB(title, text);
256353
return;
257354
}
258355
INTER_BLOCK(ob);
259
- blob_appendf(ob, "<h%d>", level);
356
+ html_new_heading(pCtx, text, level);
357
+ pHdng = pCtx->pLast;
358
+ if( pHdng->nth ){
359
+ blob_appendf(ob, "<h%d id='%h-%d'>", level, pHdng->zTag, pHdng->nth);
360
+ }else{
361
+ blob_appendf(ob, "<h%d id='%h'>", level, pHdng->zTag);
362
+ }
260363
if( pCtx->iHdngNums && level>=pCtx->iHdngNums ){
261364
int i;
262365
for(i=pCtx->iHdngNums-1; i<level-1; i++){
263366
blob_appendf(ob,"%d.",pCtx->aNum[i]);
264367
}
@@ -607,10 +710,53 @@
607710
608711
609712
static void html_normal_text(struct Blob *ob, struct Blob *text, void *opaque){
610713
html_escape(ob, blob_buffer(text), blob_size(text));
611714
}
715
+
716
+/*
717
+** Insert a table of contents into the body of the document.
718
+**
719
+** The pCtx provides the information needed to do this:
720
+**
721
+** pCtx->iToc Offset into pOut of where to insert the TOC
722
+** pCtx->mxToc Maximum depth of the TOC
723
+** pCtx->pFirst List of paragraphs to form the TOC
724
+*/
725
+static void html_insert_toc(MarkdownToHtml *pCtx, Blob *pOut){
726
+ Blob new;
727
+ MarkdownHeading *pX;
728
+ int iLevel = pCtx->mnLevel-1;
729
+ int iBase = iLevel;
730
+ blob_init(&new, 0, 0);
731
+ blob_append(&new, blob_buffer(pOut), pCtx->iToc);
732
+ blob_append(&new, "<div class='markdown-toc'>\n", -1);
733
+ for(pX=pCtx->pFirst; pX; pX=pX->pNext){
734
+ if( pX->iLevel>pCtx->mxToc ) continue;
735
+ while( iLevel<pX->iLevel ){
736
+ iLevel++;
737
+ blob_appendf(&new, "<ul class='markdown-toc%d markdown-toc'>\n",
738
+ iLevel - iBase);
739
+ }
740
+ while( iLevel>pX->iLevel ){
741
+ iLevel--;
742
+ blob_appendf(&new, "</ul>\n");
743
+ }
744
+ blob_appendf(&new,"<li><a href='#%h'>", pX->zTag);
745
+ html_to_plaintext(pX->zTitle, &new);
746
+ blob_appendf(&new,"</a></li>\n");
747
+ }
748
+ while( iLevel>iBase ){
749
+ iLevel--;
750
+ blob_appendf(&new, "</ul>\n");
751
+ }
752
+ blob_appendf(&new, "</div>\n");
753
+ blob_append(&new, blob_buffer(pOut)+pCtx->iToc,
754
+ blob_size(pOut)-pCtx->iToc);
755
+ blob_reset(pOut);
756
+ *pOut = new;
757
+}
612758
613759
/*
614760
** Convert markdown into HTML.
615761
**
616762
** The document title is placed in output_title if not NULL. Or if
@@ -665,10 +811,11 @@
665811
context.output_title = output_title;
666812
html_renderer.opaque = &context;
667813
if( output_title ) blob_reset(output_title);
668814
blob_reset(output_body);
669815
markdown(output_body, input_markdown, &html_renderer);
816
+ if( context.mxToc>0 ) html_insert_toc(&context, output_body);
670817
for(pHdng=context.pFirst; pHdng; pHdng=pNextHdng){
671818
pNextHdng = pHdng->pNext;
672819
fossil_free(pHdng);
673820
}
674821
}
675822
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -38,26 +38,122 @@
38 typedef struct MarkdownHeading MarkdownHeading;
39 struct MarkdownHeading {
40 MarkdownHeading *pPrev, *pNext; /* List of them all */
41 char *zTitle; /* Text as displayed */
42 char *zTag; /* Pandoc-style tag */
 
 
43 };
44
45 /*
46 ** An instance of the following structure is passed through the
47 ** "opaque" pointer.
48 */
49 typedef struct MarkdownToHtml MarkdownToHtml;
50 struct MarkdownToHtml {
51 Blob *output_title; /* Store the title here */
52 MarkdownHeading *pFirst, *pList; /* List of all headings */
53 int iToc; /* Where to insert table-of-contents */
54 int mxToc; /* Maximum table-of-content level */
 
55 int iHdngNums; /* True to automatically number headings */
56 int aNum[6]; /* Most recent number at each level */
57 };
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
60 /* INTER_BLOCK -- skip a line between block level elements */
61 #define INTER_BLOCK(ob) \
62 do { if( blob_size(ob)>0 ) blob_append_char(ob, '\n'); } while (0)
63
@@ -246,19 +342,26 @@
246 struct Blob *text,
247 int level,
248 void *opaque
249 ){
250 MarkdownToHtml *pCtx = (MarkdownToHtml*)opaque;
 
251 struct Blob *title = pCtx->output_title;
252 /* The first header at the beginning of a text is considered as
253 * a title and not output. */
254 if( blob_size(ob)<=PROLOG_SIZE && title!=0 && blob_size(title)==0 ){
255 BLOB_APPEND_BLOB(title, text);
256 return;
257 }
258 INTER_BLOCK(ob);
259 blob_appendf(ob, "<h%d>", level);
 
 
 
 
 
 
260 if( pCtx->iHdngNums && level>=pCtx->iHdngNums ){
261 int i;
262 for(i=pCtx->iHdngNums-1; i<level-1; i++){
263 blob_appendf(ob,"%d.",pCtx->aNum[i]);
264 }
@@ -607,10 +710,53 @@
607
608
609 static void html_normal_text(struct Blob *ob, struct Blob *text, void *opaque){
610 html_escape(ob, blob_buffer(text), blob_size(text));
611 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
613 /*
614 ** Convert markdown into HTML.
615 **
616 ** The document title is placed in output_title if not NULL. Or if
@@ -665,10 +811,11 @@
665 context.output_title = output_title;
666 html_renderer.opaque = &context;
667 if( output_title ) blob_reset(output_title);
668 blob_reset(output_body);
669 markdown(output_body, input_markdown, &html_renderer);
 
670 for(pHdng=context.pFirst; pHdng; pHdng=pNextHdng){
671 pNextHdng = pHdng->pNext;
672 fossil_free(pHdng);
673 }
674 }
675
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -38,26 +38,122 @@
38 typedef struct MarkdownHeading MarkdownHeading;
39 struct MarkdownHeading {
40 MarkdownHeading *pPrev, *pNext; /* List of them all */
41 char *zTitle; /* Text as displayed */
42 char *zTag; /* Pandoc-style tag */
43 int iLevel; /* Level number for this entry */
44 int nth; /* This is the nth with the same tag */
45 };
46
47 /*
48 ** An instance of the following structure is passed through the
49 ** "opaque" pointer.
50 */
51 typedef struct MarkdownToHtml MarkdownToHtml;
52 struct MarkdownToHtml {
53 Blob *output_title; /* Store the title here */
54 MarkdownHeading *pFirst, *pLast; /* List of all headings */
55 int iToc; /* Where to insert table-of-contents */
56 int mxToc; /* Maximum table-of-content level */
57 int mnLevel; /* Minimum level seen over all headings */
58 int iHdngNums; /* True to automatically number headings */
59 int aNum[6]; /* Most recent number at each level */
60 };
61
62 /*
63 ** Add a new heading to the heading list. This involves generating
64 ** a Pandoc-compatible identifier based on the heading text.
65 */
66 static void html_new_heading(MarkdownToHtml *pCtx, Blob *text, int iLevel){
67 MarkdownHeading *pNew, *pSearch;
68 int nText = blob_size(text);
69 size_t n = sizeof(*pNew) + nText*2 + 10;
70 const char *zText = blob_buffer(text);
71 char *zTag;
72 int i, j;
73 int seenChar = 0;
74
75 pNew = fossil_malloc( n );
76 memset(pNew, 0, n);
77 if( pCtx->pLast ){
78 pCtx->pLast->pNext = pNew;
79 if( pCtx->mnLevel>iLevel ) pCtx->mnLevel = iLevel;
80 }else{
81 pCtx->mnLevel = iLevel;
82 }
83 pNew->pPrev = pCtx->pLast;
84 pCtx->pLast = pNew;
85 if( pCtx->pFirst==0 ) pCtx->pFirst = pNew;
86 pNew->zTitle = (char*)&pNew[1];
87 memcpy(pNew->zTitle, zText, nText);
88 pNew->zTitle[nText] = 0;
89 pNew->zTag = pNew->zTitle + nText + 1;
90 pNew->iLevel = iLevel;
91 pNew->nth = 0;
92
93 /* Generate an identifier. The identifer name is approximately the
94 ** same as a Pandoc identifier.
95 **
96 ** * Skip all text up to the first letter.
97 ** * Remove all text past the last letter.
98 ** * Remove HTML markup and entities.
99 ** * Replace all whitespace sequences with a single "-"
100 ** * Remove all characters other than alphanumeric, "_", "-", and ".".
101 ** * Convert all alphabetics to lower case.
102 ** * If nothing remains, use "section" as the identifier.
103 */
104 while( nText>0 && !fossil_isalpha(zText[nText-1]) ){ nText--; }
105 memcpy(pNew->zTag, zText, nText);
106 pNew->zTag[nText] = 0;
107 zTag = pNew->zTag;
108 for(i=j=0; zTag[i]; i++){
109 if( fossil_isupper(zTag[i]) ){
110 if( !seenChar ){ j = 0; seenChar = 1; }
111 zTag[j++] = fossil_tolower(zTag[i]);
112 continue;
113 }
114 if( fossil_islower(zTag[i]) ){
115 if( !seenChar ){ j = 0; seenChar = 1; }
116 zTag[j++] = zTag[i];
117 continue;
118 }
119 if( zTag[i]=='<' ){
120 i += html_tag_length(zTag+i) - 1;
121 continue;
122 }
123 if( zTag[i]=='&' ){
124 while( zTag[i] && zTag[i]!=';' ){ i++; }
125 if( zTag[i]==0 ) break;
126 continue;
127 }
128 if( fossil_isspace(zTag[i]) ){
129 zTag[j++] = '-';
130 while( fossil_isspace(zTag[i+1]) ){ i++; }
131 continue;
132 }
133 if( !fossil_isalnum(zTag[i]) && zTag[i]!='.' && zTag[i]!='_' ){
134 zTag[j++] = '-';
135 }else{
136 zTag[j++] = zTag[i];
137 }
138 }
139 if( j==0 || !seenChar ){
140 memcpy(zTag, "section", 7);
141 j = 7;
142 }
143 while( j>0 && !fossil_isalpha(zTag[j-1]) ){ j--; }
144 zTag[j] = 0;
145
146 /* Search for duplicate identifiers and disambiguate */
147 pNew->nth = 0;
148 for(pSearch=pNew->pPrev; pSearch; pSearch=pSearch->pPrev){
149 if( strcmp(pSearch->zTag,zTag)==0 ){
150 pNew->nth = pSearch->nth+1;
151 }
152 }
153 }
154
155
156 /* INTER_BLOCK -- skip a line between block level elements */
157 #define INTER_BLOCK(ob) \
158 do { if( blob_size(ob)>0 ) blob_append_char(ob, '\n'); } while (0)
159
@@ -246,19 +342,26 @@
342 struct Blob *text,
343 int level,
344 void *opaque
345 ){
346 MarkdownToHtml *pCtx = (MarkdownToHtml*)opaque;
347 MarkdownHeading *pHdng;
348 struct Blob *title = pCtx->output_title;
349 /* The first header at the beginning of a text is considered as
350 * a title and not output. */
351 if( blob_size(ob)<=PROLOG_SIZE && title!=0 && blob_size(title)==0 ){
352 BLOB_APPEND_BLOB(title, text);
353 return;
354 }
355 INTER_BLOCK(ob);
356 html_new_heading(pCtx, text, level);
357 pHdng = pCtx->pLast;
358 if( pHdng->nth ){
359 blob_appendf(ob, "<h%d id='%h-%d'>", level, pHdng->zTag, pHdng->nth);
360 }else{
361 blob_appendf(ob, "<h%d id='%h'>", level, pHdng->zTag);
362 }
363 if( pCtx->iHdngNums && level>=pCtx->iHdngNums ){
364 int i;
365 for(i=pCtx->iHdngNums-1; i<level-1; i++){
366 blob_appendf(ob,"%d.",pCtx->aNum[i]);
367 }
@@ -607,10 +710,53 @@
710
711
712 static void html_normal_text(struct Blob *ob, struct Blob *text, void *opaque){
713 html_escape(ob, blob_buffer(text), blob_size(text));
714 }
715
716 /*
717 ** Insert a table of contents into the body of the document.
718 **
719 ** The pCtx provides the information needed to do this:
720 **
721 ** pCtx->iToc Offset into pOut of where to insert the TOC
722 ** pCtx->mxToc Maximum depth of the TOC
723 ** pCtx->pFirst List of paragraphs to form the TOC
724 */
725 static void html_insert_toc(MarkdownToHtml *pCtx, Blob *pOut){
726 Blob new;
727 MarkdownHeading *pX;
728 int iLevel = pCtx->mnLevel-1;
729 int iBase = iLevel;
730 blob_init(&new, 0, 0);
731 blob_append(&new, blob_buffer(pOut), pCtx->iToc);
732 blob_append(&new, "<div class='markdown-toc'>\n", -1);
733 for(pX=pCtx->pFirst; pX; pX=pX->pNext){
734 if( pX->iLevel>pCtx->mxToc ) continue;
735 while( iLevel<pX->iLevel ){
736 iLevel++;
737 blob_appendf(&new, "<ul class='markdown-toc%d markdown-toc'>\n",
738 iLevel - iBase);
739 }
740 while( iLevel>pX->iLevel ){
741 iLevel--;
742 blob_appendf(&new, "</ul>\n");
743 }
744 blob_appendf(&new,"<li><a href='#%h'>", pX->zTag);
745 html_to_plaintext(pX->zTitle, &new);
746 blob_appendf(&new,"</a></li>\n");
747 }
748 while( iLevel>iBase ){
749 iLevel--;
750 blob_appendf(&new, "</ul>\n");
751 }
752 blob_appendf(&new, "</div>\n");
753 blob_append(&new, blob_buffer(pOut)+pCtx->iToc,
754 blob_size(pOut)-pCtx->iToc);
755 blob_reset(pOut);
756 *pOut = new;
757 }
758
759 /*
760 ** Convert markdown into HTML.
761 **
762 ** The document title is placed in output_title if not NULL. Or if
@@ -665,10 +811,11 @@
811 context.output_title = output_title;
812 html_renderer.opaque = &context;
813 if( output_title ) blob_reset(output_title);
814 blob_reset(output_body);
815 markdown(output_body, input_markdown, &html_renderer);
816 if( context.mxToc>0 ) html_insert_toc(&context, output_body);
817 for(pHdng=context.pFirst; pHdng; pHdng=pNextHdng){
818 pNextHdng = pHdng->pNext;
819 fossil_free(pHdng);
820 }
821 }
822

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button