Fossil SCM

If several footnotes are defined with the same label then join them into a single footnote. Text from each definition becomes an item in the list. This solution makes such situations noticable for the usual case (when this is an oversight) but also not obtrusive for the rare cases (when this is intentional). The list is provided with a special class to enable styling via skin customization.<br><b>This check-in is known to cause crash, see the forthcoming check-in.</b>

george 2022-02-08 14:04 markdown-footnotes
Commit 544df852b2d9a1e8326b7792f5f9b6d1da7eb74768297c3b267352ffd1e59d5b
+73 -12
--- src/markdown.c
+++ src/markdown.c
@@ -180,11 +180,11 @@
180180
181181
struct {
182182
Blob all; /* array of footnotes */
183183
int nLbled; /* number of labeled footnotes found during the first pass */
184184
int nMarks; /* counts distinct indices found during the second pass */
185
- struct footnote misref; /* nUsed counts misreferences, index must be -1 */
185
+ struct footnote misref; /* nUsed counts misreferences, iMark must be -1 */
186186
} notes;
187187
};
188188
189189
/* html_tag -- structure for quick HTML tag search (inspired from discount) */
190190
struct html_tag {
@@ -271,10 +271,28 @@
271271
static int cmp_link_ref_sort(const void *a, const void *b){
272272
struct link_ref *lra = (void *)a;
273273
struct link_ref *lrb = (void *)b;
274274
return blob_compare(&lra->id, &lrb->id);
275275
}
276
+
277
+/* cmp_footnote_id -- comparison function for footnotes qsort.
278
+ * Empty IDs sort last (in undetermined order).
279
+ * Equal IDs are sorted in the REVERSED order of definition in the source */
280
+static int cmp_footnote_id(const void *fna, const void *fnb){
281
+ const struct footnote *a = fna, *b = fnb;
282
+ const int szA = blob_size(&a->id), szB = blob_size(&b->id);
283
+ if( szA ){
284
+ if( szB ){
285
+ int cmp = blob_compare(&a->id, &b->id);
286
+ if( cmp ) return cmp;
287
+ }else return -1;
288
+ }else return szB ? 1 : 0;
289
+ /* ids are equal and non-empty */
290
+ if( a->defno < b->defno ) return -1;
291
+ if( a->defno > b->defno ) return 1;
292
+ return 0; /* should never reach here */
293
+}
276294
277295
/* cmp_footnote_sort -- comparison function for footnotes qsort.
278296
* Unreferenced footnotes (when nUsed == 0) sort last and
279297
* are sorted in the order of definition in the source */
280298
static int cmp_footnote_sort(const void *fna, const void *fnb){
@@ -1053,13 +1071,13 @@
10531071
blob_append(link, blob_buffer(&lr->link), blob_size(&lr->link));
10541072
blob_append(title, blob_buffer(&lr->title), blob_size(&lr->title));
10551073
return 0;
10561074
}
10571075
1058
-/* get_footnote() is invoked during the second pass
1059
- * on success: fill text and return positive footnote's index
1060
- * on failure: return -1 */
1076
+/* get_footnote() -- find a footnote by label, invoked during the 2nd pass.
1077
+ * On success returns a footnote (after incrementing its nUsed field),
1078
+ * otherwise returns NULL */
10611079
static const struct footnote* get_footnote(
10621080
struct render *rndr,
10631081
const char *data,
10641082
size_t size
10651083
){
@@ -1081,10 +1099,11 @@
10811099
assert( fn->nUsed > 0 );
10821100
cleanup:
10831101
release_work_buffer( rndr, id );
10841102
return fn;
10851103
}
1104
+
10861105
/* Adds unlabeled footnote to the rndr.
10871106
* If text is blank then returns 0,
10881107
* otherwise returns the address of the added footnote. */
10891108
static inline const struct footnote* add_inline_footnote(
10901109
struct render *rndr,
@@ -2538,29 +2557,71 @@
25382557
end += 1;
25392558
}
25402559
beg = end;
25412560
}
25422561
}
2543
- assert( rndr.notes.nMarks==0 );
2562
+
25442563
/* sorting the reference array */
25452564
if( blob_size(&rndr.refs) ){
25462565
qsort(blob_buffer(&rndr.refs),
25472566
blob_size(&rndr.refs)/sizeof(struct link_ref),
25482567
sizeof(struct link_ref),
25492568
cmp_link_ref_sort);
25502569
}
25512570
rndr.notes.nLbled = COUNT_FOOTNOTES(&rndr.notes.all);
2552
- /* sorting the footnotes array by id */
2553
- if( rndr.notes.nLbled ){
2571
+
2572
+ /* sort footnotes by ID and join duplicates */
2573
+ if( rndr.notes.nLbled > 1 ){
25542574
fn = CAST_AS_FOOTNOTES(&rndr.notes.all);
2555
- qsort(fn, rndr.notes.nLbled, sizeof(struct footnote),
2556
- cmp_link_ref_sort);
2557
- for(i=0; i<rndr.notes.nLbled; i++){
2558
- fn[i].index = i;
2575
+ qsort(fn, rndr.notes.nLbled, sizeof(struct footnote), cmp_footnote_id);
2576
+
2577
+ /* concatenate footnotes with equal labels */
2578
+ for(i=0; i<rndr.notes.nLbled ;){
2579
+ struct footnote *x = fn + i;
2580
+ size_t j = i+1, k = blob_size(&x->text) + 64;
2581
+ while(j<rndr.notes.nLbled && !blob_compare(&x->id, &fn[j].id)){
2582
+ k += blob_size(&fn[j].text) + 10;
2583
+ j++;
2584
+ }
2585
+ if( i+1<j ){
2586
+ Blob tmp = empty_blob;
2587
+ blob_reserve(&tmp, k);
2588
+ blob_append_string(&tmp, "<ul class='footnote-joined'>\n");
2589
+ for(k=i; k<j; k++){
2590
+ struct footnote *y = fn + k;
2591
+ blob_append_string(&tmp, "<li>");
2592
+ blob_append(&tmp, blob_buffer(&y->text), blob_size(&y->text));
2593
+ blob_append_string(&tmp, "</li>\n");
2594
+
2595
+ /* free memory buffer */
2596
+ blob_reset(&y->text);
2597
+ if( k!=i ){
2598
+ blob_reset(&y->id);
2599
+ /* invalidate redundant elements (this is optional) */
2600
+ memset(y,0,sizeof(struct footnote));
2601
+ y->index = y->defno = y->iMark = y->nUsed = -42;
2602
+ }
2603
+ }
2604
+ blob_append_string(&tmp, "</ul>\n");
2605
+ x->text = tmp;
2606
+ }
2607
+ i = j;
25592608
}
2560
- /* FIXME: handle footnotes with duplicated labels */
2609
+
2610
+ /* move redundant elements to the end of array and truncate/resize */
2611
+ qsort(fn, rndr.notes.nLbled, sizeof(struct footnote), cmp_footnote_id);
2612
+ i = rndr.notes.nLbled;
2613
+ while( i && !blob_size(&fn[i-1].id) ){ i--; }
2614
+ rndr.notes.nLbled = i;
2615
+ blob_truncate( &rndr.notes.all, i*sizeof(struct footnote) );
2616
+ }
2617
+ assert( COUNT_FOOTNOTES(&rndr.notes.all) == rndr.notes.nLbled );
2618
+ fn = CAST_AS_FOOTNOTES(&rndr.notes.all);
2619
+ for(i=0; i<rndr.notes.nLbled; i++){
2620
+ fn[i].index = i;
25612621
}
2622
+ assert( rndr.notes.nMarks==0 );
25622623
25632624
/* second pass: actual rendering */
25642625
if( rndr.make.prolog ) rndr.make.prolog(ob, rndr.make.opaque);
25652626
parse_block(ob, &rndr, blob_buffer(&text), blob_size(&text));
25662627
25672628
--- src/markdown.c
+++ src/markdown.c
@@ -180,11 +180,11 @@
180
181 struct {
182 Blob all; /* array of footnotes */
183 int nLbled; /* number of labeled footnotes found during the first pass */
184 int nMarks; /* counts distinct indices found during the second pass */
185 struct footnote misref; /* nUsed counts misreferences, index must be -1 */
186 } notes;
187 };
188
189 /* html_tag -- structure for quick HTML tag search (inspired from discount) */
190 struct html_tag {
@@ -271,10 +271,28 @@
271 static int cmp_link_ref_sort(const void *a, const void *b){
272 struct link_ref *lra = (void *)a;
273 struct link_ref *lrb = (void *)b;
274 return blob_compare(&lra->id, &lrb->id);
275 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
277 /* cmp_footnote_sort -- comparison function for footnotes qsort.
278 * Unreferenced footnotes (when nUsed == 0) sort last and
279 * are sorted in the order of definition in the source */
280 static int cmp_footnote_sort(const void *fna, const void *fnb){
@@ -1053,13 +1071,13 @@
1053 blob_append(link, blob_buffer(&lr->link), blob_size(&lr->link));
1054 blob_append(title, blob_buffer(&lr->title), blob_size(&lr->title));
1055 return 0;
1056 }
1057
1058 /* get_footnote() is invoked during the second pass
1059 * on success: fill text and return positive footnote's index
1060 * on failure: return -1 */
1061 static const struct footnote* get_footnote(
1062 struct render *rndr,
1063 const char *data,
1064 size_t size
1065 ){
@@ -1081,10 +1099,11 @@
1081 assert( fn->nUsed > 0 );
1082 cleanup:
1083 release_work_buffer( rndr, id );
1084 return fn;
1085 }
 
1086 /* Adds unlabeled footnote to the rndr.
1087 * If text is blank then returns 0,
1088 * otherwise returns the address of the added footnote. */
1089 static inline const struct footnote* add_inline_footnote(
1090 struct render *rndr,
@@ -2538,29 +2557,71 @@
2538 end += 1;
2539 }
2540 beg = end;
2541 }
2542 }
2543 assert( rndr.notes.nMarks==0 );
2544 /* sorting the reference array */
2545 if( blob_size(&rndr.refs) ){
2546 qsort(blob_buffer(&rndr.refs),
2547 blob_size(&rndr.refs)/sizeof(struct link_ref),
2548 sizeof(struct link_ref),
2549 cmp_link_ref_sort);
2550 }
2551 rndr.notes.nLbled = COUNT_FOOTNOTES(&rndr.notes.all);
2552 /* sorting the footnotes array by id */
2553 if( rndr.notes.nLbled ){
 
2554 fn = CAST_AS_FOOTNOTES(&rndr.notes.all);
2555 qsort(fn, rndr.notes.nLbled, sizeof(struct footnote),
2556 cmp_link_ref_sort);
2557 for(i=0; i<rndr.notes.nLbled; i++){
2558 fn[i].index = i;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2559 }
2560 /* FIXME: handle footnotes with duplicated labels */
 
 
 
 
 
 
 
 
 
 
 
2561 }
 
2562
2563 /* second pass: actual rendering */
2564 if( rndr.make.prolog ) rndr.make.prolog(ob, rndr.make.opaque);
2565 parse_block(ob, &rndr, blob_buffer(&text), blob_size(&text));
2566
2567
--- src/markdown.c
+++ src/markdown.c
@@ -180,11 +180,11 @@
180
181 struct {
182 Blob all; /* array of footnotes */
183 int nLbled; /* number of labeled footnotes found during the first pass */
184 int nMarks; /* counts distinct indices found during the second pass */
185 struct footnote misref; /* nUsed counts misreferences, iMark must be -1 */
186 } notes;
187 };
188
189 /* html_tag -- structure for quick HTML tag search (inspired from discount) */
190 struct html_tag {
@@ -271,10 +271,28 @@
271 static int cmp_link_ref_sort(const void *a, const void *b){
272 struct link_ref *lra = (void *)a;
273 struct link_ref *lrb = (void *)b;
274 return blob_compare(&lra->id, &lrb->id);
275 }
276
277 /* cmp_footnote_id -- comparison function for footnotes qsort.
278 * Empty IDs sort last (in undetermined order).
279 * Equal IDs are sorted in the REVERSED order of definition in the source */
280 static int cmp_footnote_id(const void *fna, const void *fnb){
281 const struct footnote *a = fna, *b = fnb;
282 const int szA = blob_size(&a->id), szB = blob_size(&b->id);
283 if( szA ){
284 if( szB ){
285 int cmp = blob_compare(&a->id, &b->id);
286 if( cmp ) return cmp;
287 }else return -1;
288 }else return szB ? 1 : 0;
289 /* ids are equal and non-empty */
290 if( a->defno < b->defno ) return -1;
291 if( a->defno > b->defno ) return 1;
292 return 0; /* should never reach here */
293 }
294
295 /* cmp_footnote_sort -- comparison function for footnotes qsort.
296 * Unreferenced footnotes (when nUsed == 0) sort last and
297 * are sorted in the order of definition in the source */
298 static int cmp_footnote_sort(const void *fna, const void *fnb){
@@ -1053,13 +1071,13 @@
1071 blob_append(link, blob_buffer(&lr->link), blob_size(&lr->link));
1072 blob_append(title, blob_buffer(&lr->title), blob_size(&lr->title));
1073 return 0;
1074 }
1075
1076 /* get_footnote() -- find a footnote by label, invoked during the 2nd pass.
1077 * On success returns a footnote (after incrementing its nUsed field),
1078 * otherwise returns NULL */
1079 static const struct footnote* get_footnote(
1080 struct render *rndr,
1081 const char *data,
1082 size_t size
1083 ){
@@ -1081,10 +1099,11 @@
1099 assert( fn->nUsed > 0 );
1100 cleanup:
1101 release_work_buffer( rndr, id );
1102 return fn;
1103 }
1104
1105 /* Adds unlabeled footnote to the rndr.
1106 * If text is blank then returns 0,
1107 * otherwise returns the address of the added footnote. */
1108 static inline const struct footnote* add_inline_footnote(
1109 struct render *rndr,
@@ -2538,29 +2557,71 @@
2557 end += 1;
2558 }
2559 beg = end;
2560 }
2561 }
2562
2563 /* sorting the reference array */
2564 if( blob_size(&rndr.refs) ){
2565 qsort(blob_buffer(&rndr.refs),
2566 blob_size(&rndr.refs)/sizeof(struct link_ref),
2567 sizeof(struct link_ref),
2568 cmp_link_ref_sort);
2569 }
2570 rndr.notes.nLbled = COUNT_FOOTNOTES(&rndr.notes.all);
2571
2572 /* sort footnotes by ID and join duplicates */
2573 if( rndr.notes.nLbled > 1 ){
2574 fn = CAST_AS_FOOTNOTES(&rndr.notes.all);
2575 qsort(fn, rndr.notes.nLbled, sizeof(struct footnote), cmp_footnote_id);
2576
2577 /* concatenate footnotes with equal labels */
2578 for(i=0; i<rndr.notes.nLbled ;){
2579 struct footnote *x = fn + i;
2580 size_t j = i+1, k = blob_size(&x->text) + 64;
2581 while(j<rndr.notes.nLbled && !blob_compare(&x->id, &fn[j].id)){
2582 k += blob_size(&fn[j].text) + 10;
2583 j++;
2584 }
2585 if( i+1<j ){
2586 Blob tmp = empty_blob;
2587 blob_reserve(&tmp, k);
2588 blob_append_string(&tmp, "<ul class='footnote-joined'>\n");
2589 for(k=i; k<j; k++){
2590 struct footnote *y = fn + k;
2591 blob_append_string(&tmp, "<li>");
2592 blob_append(&tmp, blob_buffer(&y->text), blob_size(&y->text));
2593 blob_append_string(&tmp, "</li>\n");
2594
2595 /* free memory buffer */
2596 blob_reset(&y->text);
2597 if( k!=i ){
2598 blob_reset(&y->id);
2599 /* invalidate redundant elements (this is optional) */
2600 memset(y,0,sizeof(struct footnote));
2601 y->index = y->defno = y->iMark = y->nUsed = -42;
2602 }
2603 }
2604 blob_append_string(&tmp, "</ul>\n");
2605 x->text = tmp;
2606 }
2607 i = j;
2608 }
2609
2610 /* move redundant elements to the end of array and truncate/resize */
2611 qsort(fn, rndr.notes.nLbled, sizeof(struct footnote), cmp_footnote_id);
2612 i = rndr.notes.nLbled;
2613 while( i && !blob_size(&fn[i-1].id) ){ i--; }
2614 rndr.notes.nLbled = i;
2615 blob_truncate( &rndr.notes.all, i*sizeof(struct footnote) );
2616 }
2617 assert( COUNT_FOOTNOTES(&rndr.notes.all) == rndr.notes.nLbled );
2618 fn = CAST_AS_FOOTNOTES(&rndr.notes.all);
2619 for(i=0; i<rndr.notes.nLbled; i++){
2620 fn[i].index = i;
2621 }
2622 assert( rndr.notes.nMarks==0 );
2623
2624 /* second pass: actual rendering */
2625 if( rndr.make.prolog ) rndr.make.prolog(ob, rndr.make.opaque);
2626 parse_block(ob, &rndr, blob_buffer(&text), blob_size(&text));
2627
2628
--- test/markdown-test3.md
+++ test/markdown-test3.md
@@ -10,10 +10,12 @@
1010
Developers are invited to add test cases here[^here].
1111
It is suggested that the more simple is a test case the earlier it should
1212
appear in this document.[^ if glitch occurs ]
1313
1414
[^lost3]: This note was defined at the begining of the document.
15
+
16
+[^duplicate]: This came from the begining of the document.
1517
1618
A footnote's label should be case insensitive[^ case INSENSITIVE ],
1719
it is whitespace-savvy and can even contain newlines.[^ a
1820
multiline
1921
label]
@@ -39,10 +41,13 @@
3941
This can be overridden by the skin though.
4042
4143
The refenrence at the end of this sentence is the sole reason of
4244
rendering of <s>`lost1` and</s> [lost2][^].
4345
46
+If several labeled footnote definitions have the same equal label then texts
47
+from all these definitions are joined.[^duplicate]
48
+
4449
## Footnotes
4550
4651
[branch]: /timeline?r=markdown-footnotes&nowiki
4752
4853
[^ 1]: Footnotes is a Fossil' extention of
@@ -58,10 +63,11 @@
5863
5964
[^ a multiline label ]: But at a footnote's definition it should still
6065
be written within square brackets
6166
on a single line.
6267
68
+[^duplicate]: And that came from the end of the document.
6369
6470
[^many-refs]:
6571
Each letter on the left is a back-reference to the place of use.
6672
Highlighted back-reference indicates a place from which navigation
6773
occurred[^lost1].
6874
--- test/markdown-test3.md
+++ test/markdown-test3.md
@@ -10,10 +10,12 @@
10 Developers are invited to add test cases here[^here].
11 It is suggested that the more simple is a test case the earlier it should
12 appear in this document.[^ if glitch occurs ]
13
14 [^lost3]: This note was defined at the begining of the document.
 
 
15
16 A footnote's label should be case insensitive[^ case INSENSITIVE ],
17 it is whitespace-savvy and can even contain newlines.[^ a
18 multiline
19 label]
@@ -39,10 +41,13 @@
39 This can be overridden by the skin though.
40
41 The refenrence at the end of this sentence is the sole reason of
42 rendering of <s>`lost1` and</s> [lost2][^].
43
 
 
 
44 ## Footnotes
45
46 [branch]: /timeline?r=markdown-footnotes&nowiki
47
48 [^ 1]: Footnotes is a Fossil' extention of
@@ -58,10 +63,11 @@
58
59 [^ a multiline label ]: But at a footnote's definition it should still
60 be written within square brackets
61 on a single line.
62
 
63
64 [^many-refs]:
65 Each letter on the left is a back-reference to the place of use.
66 Highlighted back-reference indicates a place from which navigation
67 occurred[^lost1].
68
--- test/markdown-test3.md
+++ test/markdown-test3.md
@@ -10,10 +10,12 @@
10 Developers are invited to add test cases here[^here].
11 It is suggested that the more simple is a test case the earlier it should
12 appear in this document.[^ if glitch occurs ]
13
14 [^lost3]: This note was defined at the begining of the document.
15
16 [^duplicate]: This came from the begining of the document.
17
18 A footnote's label should be case insensitive[^ case INSENSITIVE ],
19 it is whitespace-savvy and can even contain newlines.[^ a
20 multiline
21 label]
@@ -39,10 +41,13 @@
41 This can be overridden by the skin though.
42
43 The refenrence at the end of this sentence is the sole reason of
44 rendering of <s>`lost1` and</s> [lost2][^].
45
46 If several labeled footnote definitions have the same equal label then texts
47 from all these definitions are joined.[^duplicate]
48
49 ## Footnotes
50
51 [branch]: /timeline?r=markdown-footnotes&nowiki
52
53 [^ 1]: Footnotes is a Fossil' extention of
@@ -58,10 +63,11 @@
63
64 [^ a multiline label ]: But at a footnote's definition it should still
65 be written within square brackets
66 on a single line.
67
68 [^duplicate]: And that came from the end of the document.
69
70 [^many-refs]:
71 Each letter on the left is a back-reference to the place of use.
72 Highlighted back-reference indicates a place from which navigation
73 occurred[^lost1].
74

Keyboard Shortcuts

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