Fossil SCM

Tag labels in Markdown with IDs that are compatible with GitHub.

drh 2026-01-13 19:52 trunk merge
Commit f9ead7530c6e61be8ce795e5bc65f8e5c36743775bc76217c383ca032aa01c58
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -216,20 +216,61 @@
216216
struct Blob *text,
217217
int level,
218218
void *opaque
219219
){
220220
struct Blob *title = ((MarkdownToHtml*)opaque)->output_title;
221
+ char *z = 0;
222
+ int i,j;
221223
/* The first header at the beginning of a text is considered as
222224
* a title and not output. */
223225
if( blob_size(ob)<=PROLOG_SIZE && title!=0 && blob_size(title)==0 ){
224226
blob_appendb(title, text);
225227
return;
226228
}
227229
INTER_BLOCK(ob);
228
- blob_appendf(ob, "<h%d>", level);
230
+ z = fossil_strdup(blob_buffer(text));
231
+ if( z==0 ){
232
+ j = 0;
233
+ }else{
234
+ /*
235
+ ** The GitHub "slugify" algorithm converts the text of a markdown header
236
+ ** into a ID for that header. The algorithm is:
237
+ **
238
+ ** 1. ASCII alphanumerics -> convert to lower case
239
+ ** 2. Spaces, hyphens, underscores -> convert to '-'
240
+ ** 3. Non-ASCII -> preserve as-is
241
+ ** 4. Other punctuation -> remove
242
+ ** 5. Multiple consecutive dashes -> collapse to one
243
+ ** 6. Leading and trailing dashes -> remove
244
+ ** 7. Markup <...> and &...; -> remove
245
+ **
246
+ ** This implementation does the conversion in-place.
247
+ */
248
+ for(i=j=0; z[i]; i++){
249
+ if( fossil_isalnum(z[i]) ){
250
+ z[j++] = fossil_tolower(z[i]);
251
+ }else if( fossil_isspace(z[i]) || z[i]=='-' || z[i]=='_' ){
252
+ if( j>0 && z[j-1]!='-' ) z[j++] = '-';
253
+ }else if( z[i]=='<' ){
254
+ do{ i++; }while( z[i]!=0 && z[i]!='>' );
255
+ }else if( z[i]=='&' ){
256
+ do{ i++; }while( z[i]!=0 && z[i]!=';' );
257
+ }else if( (z[i]&0x80)!=0 ){
258
+ z[j++] = z[i];
259
+ }
260
+ }
261
+ if( j>0 && z[j-1]=='-' ) j--;
262
+ z[j] = 0;
263
+ }
264
+ if( j>0 ){
265
+ blob_appendf(ob, "<h%d id=\"%s\">", level, z);
266
+ }else{
267
+ blob_appendf(ob, "<h%d>", level);
268
+ }
229269
blob_appendb(ob, text);
230270
blob_appendf(ob, "</h%d>", level);
271
+ fossil_free(z);
231272
}
232273
233274
static void html_hrule(struct Blob *ob, void *opaque){
234275
INTER_BLOCK(ob);
235276
blob_append_literal(ob, "<hr>\n");
236277
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -216,20 +216,61 @@
216 struct Blob *text,
217 int level,
218 void *opaque
219 ){
220 struct Blob *title = ((MarkdownToHtml*)opaque)->output_title;
 
 
221 /* The first header at the beginning of a text is considered as
222 * a title and not output. */
223 if( blob_size(ob)<=PROLOG_SIZE && title!=0 && blob_size(title)==0 ){
224 blob_appendb(title, text);
225 return;
226 }
227 INTER_BLOCK(ob);
228 blob_appendf(ob, "<h%d>", level);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229 blob_appendb(ob, text);
230 blob_appendf(ob, "</h%d>", level);
 
231 }
232
233 static void html_hrule(struct Blob *ob, void *opaque){
234 INTER_BLOCK(ob);
235 blob_append_literal(ob, "<hr>\n");
236
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -216,20 +216,61 @@
216 struct Blob *text,
217 int level,
218 void *opaque
219 ){
220 struct Blob *title = ((MarkdownToHtml*)opaque)->output_title;
221 char *z = 0;
222 int i,j;
223 /* The first header at the beginning of a text is considered as
224 * a title and not output. */
225 if( blob_size(ob)<=PROLOG_SIZE && title!=0 && blob_size(title)==0 ){
226 blob_appendb(title, text);
227 return;
228 }
229 INTER_BLOCK(ob);
230 z = fossil_strdup(blob_buffer(text));
231 if( z==0 ){
232 j = 0;
233 }else{
234 /*
235 ** The GitHub "slugify" algorithm converts the text of a markdown header
236 ** into a ID for that header. The algorithm is:
237 **
238 ** 1. ASCII alphanumerics -> convert to lower case
239 ** 2. Spaces, hyphens, underscores -> convert to '-'
240 ** 3. Non-ASCII -> preserve as-is
241 ** 4. Other punctuation -> remove
242 ** 5. Multiple consecutive dashes -> collapse to one
243 ** 6. Leading and trailing dashes -> remove
244 ** 7. Markup <...> and &...; -> remove
245 **
246 ** This implementation does the conversion in-place.
247 */
248 for(i=j=0; z[i]; i++){
249 if( fossil_isalnum(z[i]) ){
250 z[j++] = fossil_tolower(z[i]);
251 }else if( fossil_isspace(z[i]) || z[i]=='-' || z[i]=='_' ){
252 if( j>0 && z[j-1]!='-' ) z[j++] = '-';
253 }else if( z[i]=='<' ){
254 do{ i++; }while( z[i]!=0 && z[i]!='>' );
255 }else if( z[i]=='&' ){
256 do{ i++; }while( z[i]!=0 && z[i]!=';' );
257 }else if( (z[i]&0x80)!=0 ){
258 z[j++] = z[i];
259 }
260 }
261 if( j>0 && z[j-1]=='-' ) j--;
262 z[j] = 0;
263 }
264 if( j>0 ){
265 blob_appendf(ob, "<h%d id=\"%s\">", level, z);
266 }else{
267 blob_appendf(ob, "<h%d>", level);
268 }
269 blob_appendb(ob, text);
270 blob_appendf(ob, "</h%d>", level);
271 fossil_free(z);
272 }
273
274 static void html_hrule(struct Blob *ob, void *opaque){
275 INTER_BLOCK(ob);
276 blob_append_literal(ob, "<hr>\n");
277
--- www/changes.wiki
+++ www/changes.wiki
@@ -44,10 +44,12 @@
4444
<li> "No-graph" timelines (using the "ng" query parameter) now show
4545
branch colors and bare check-in circles on the left. The check-in
4646
circles appear, but no lines connecting them.
4747
([/timeline?ng|example]).
4848
</ol>
49
+ <li> Labels in Markdown now have IDs generated using the GitHub "slugify"
50
+ algorithm.
4951
<li> The [/help/timeline|timeline command] is enhanced with the new options
5052
"<tt>-u|--for-user</tt>" to filter by user, and "<tt>-r</tt>" to display
5153
entries in chronological order.
5254
<li> The [/help/open|open command]'s new "<tt>--reopen REPOFILE</tt>" flag
5355
can be used to fix a checkout after moving its repository file.
5456
--- www/changes.wiki
+++ www/changes.wiki
@@ -44,10 +44,12 @@
44 <li> "No-graph" timelines (using the "ng" query parameter) now show
45 branch colors and bare check-in circles on the left. The check-in
46 circles appear, but no lines connecting them.
47 ([/timeline?ng|example]).
48 </ol>
 
 
49 <li> The [/help/timeline|timeline command] is enhanced with the new options
50 "<tt>-u|--for-user</tt>" to filter by user, and "<tt>-r</tt>" to display
51 entries in chronological order.
52 <li> The [/help/open|open command]'s new "<tt>--reopen REPOFILE</tt>" flag
53 can be used to fix a checkout after moving its repository file.
54
--- www/changes.wiki
+++ www/changes.wiki
@@ -44,10 +44,12 @@
44 <li> "No-graph" timelines (using the "ng" query parameter) now show
45 branch colors and bare check-in circles on the left. The check-in
46 circles appear, but no lines connecting them.
47 ([/timeline?ng|example]).
48 </ol>
49 <li> Labels in Markdown now have IDs generated using the GitHub "slugify"
50 algorithm.
51 <li> The [/help/timeline|timeline command] is enhanced with the new options
52 "<tt>-u|--for-user</tt>" to filter by user, and "<tt>-r</tt>" to display
53 entries in chronological order.
54 <li> The [/help/open|open command]'s new "<tt>--reopen REPOFILE</tt>" flag
55 can be used to fix a checkout after moving its repository file.
56

Keyboard Shortcuts

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