|
1
|
/* |
|
2
|
** Copyright (c) 2020 D. Richard Hipp |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
|
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** This file contains code to implement for managing backlinks and |
|
19
|
** the "backlink" table of the repository database. |
|
20
|
** |
|
21
|
** A backlink is a reference in Fossil-Wiki or Markdown to some other |
|
22
|
** object in the repository. |
|
23
|
*/ |
|
24
|
#include "config.h" |
|
25
|
#include "backlink.h" |
|
26
|
#include <assert.h> |
|
27
|
|
|
28
|
|
|
29
|
/* |
|
30
|
** Show a graph of all wiki, tickets, and check-ins that refer to object zUuid. |
|
31
|
** |
|
32
|
** If zLabel is not NULL and the graph is not empty, then output zLabel as |
|
33
|
** a prefix to the graph. |
|
34
|
*/ |
|
35
|
void render_backlink_graph(const char *zUuid, const char *zLabel){ |
|
36
|
Blob sql; |
|
37
|
Stmt q; |
|
38
|
char *zGlob; |
|
39
|
int needEndPanel = 0; |
|
40
|
zGlob = mprintf("%.5s*", zUuid); |
|
41
|
db_multi_exec( |
|
42
|
"CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);\n" |
|
43
|
"DELETE FROM ok;\n" |
|
44
|
"INSERT OR IGNORE INTO ok(rid)\n" |
|
45
|
" SELECT CASE srctype\n" |
|
46
|
" WHEN 2 THEN (SELECT rid FROM tagxref WHERE tagid=backlink.srcid\n" |
|
47
|
" ORDER BY mtime DESC LIMIT 1)\n" |
|
48
|
" ELSE srcid END\n" |
|
49
|
" FROM backlink\n" |
|
50
|
" WHERE target GLOB %Q" |
|
51
|
" AND %Q GLOB (target || '*');", |
|
52
|
zGlob, zUuid |
|
53
|
); |
|
54
|
if( !db_exists("SELECT 1 FROM ok") ) return; |
|
55
|
if( zLabel ){ |
|
56
|
cgi_printf("%s", zLabel); |
|
57
|
if( strstr(zLabel, "accordion")!=0 ){ |
|
58
|
cgi_printf("<div class=\"accordion_panel\">\n"); |
|
59
|
needEndPanel = 1; |
|
60
|
} |
|
61
|
} |
|
62
|
blob_zero(&sql); |
|
63
|
blob_append(&sql, timeline_query_for_www(), -1); |
|
64
|
blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC"); |
|
65
|
db_prepare(&q, "%s", blob_sql_text(&sql)); |
|
66
|
www_print_timeline(&q, |
|
67
|
TIMELINE_DISJOINT|TIMELINE_GRAPH|TIMELINE_NOSCROLL|TIMELINE_REFS, |
|
68
|
0, 0, 0, 0, 0, 0); |
|
69
|
db_finalize(&q); |
|
70
|
if( needEndPanel ){ |
|
71
|
cgi_printf("</div>\n"); |
|
72
|
} |
|
73
|
} |
|
74
|
|
|
75
|
/* |
|
76
|
** WEBPAGE: test-backlink-timeline |
|
77
|
** |
|
78
|
** Show a timeline of all check-ins and other events that have entries |
|
79
|
** in the backlink table. This is used for testing the rendering |
|
80
|
** of the "References" section of the /info page. |
|
81
|
*/ |
|
82
|
void backlink_timeline_page(void){ |
|
83
|
Blob sql; |
|
84
|
Stmt q; |
|
85
|
|
|
86
|
login_check_credentials(); |
|
87
|
if( !g.perm.Read || !g.perm.RdTkt || !g.perm.RdWiki ){ |
|
88
|
login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki); |
|
89
|
return; |
|
90
|
} |
|
91
|
style_set_current_feature("test"); |
|
92
|
style_header("Backlink Timeline (Internal Testing Use)"); |
|
93
|
db_multi_exec( |
|
94
|
"CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);" |
|
95
|
"DELETE FROM ok;" |
|
96
|
"INSERT OR IGNORE INTO ok" |
|
97
|
" SELECT blob.rid FROM backlink, blob" |
|
98
|
" WHERE blob.uuid BETWEEN backlink.target AND (backlink.target||'x')" |
|
99
|
); |
|
100
|
blob_zero(&sql); |
|
101
|
blob_append(&sql, timeline_query_for_www(), -1); |
|
102
|
blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC"); |
|
103
|
db_prepare(&q, "%s", blob_sql_text(&sql)); |
|
104
|
www_print_timeline(&q, TIMELINE_DISJOINT|TIMELINE_GRAPH|TIMELINE_NOSCROLL, |
|
105
|
0, 0, 0, 0, 0, 0); |
|
106
|
db_finalize(&q); |
|
107
|
style_finish_page(); |
|
108
|
} |
|
109
|
|
|
110
|
/* |
|
111
|
** WEBPAGE: test-backlinks |
|
112
|
** |
|
113
|
** Show a table of all backlinks. Admin access only. |
|
114
|
*/ |
|
115
|
void backlink_table_page(void){ |
|
116
|
Stmt q; |
|
117
|
int n; |
|
118
|
login_check_credentials(); |
|
119
|
if( !g.perm.Admin ){ |
|
120
|
login_needed(g.anon.Admin); |
|
121
|
return; |
|
122
|
} |
|
123
|
style_set_current_feature("test"); |
|
124
|
style_header("Backlink Table (Internal Testing Use)"); |
|
125
|
n = db_int(0, "SELECT count(*) FROM backlink"); |
|
126
|
@ <p>%d(n) backlink table entries:</p> |
|
127
|
db_prepare(&q, |
|
128
|
"SELECT target, srctype, srcid, datetime(mtime)," |
|
129
|
" CASE srctype" |
|
130
|
" WHEN 2 THEN (SELECT substr(tagname,6) FROM tag" |
|
131
|
" WHERE tagid=srcid AND tagname GLOB 'wiki-*')" |
|
132
|
" ELSE null END FROM backlink" |
|
133
|
); |
|
134
|
style_table_sorter(); |
|
135
|
@ <table border="1" cellpadding="2" cellspacing="0" \ |
|
136
|
@ class='sortable' data-column-types='ttt' data-init-sort='0'> |
|
137
|
@ <thead><tr><th> Target <th> Source <th> mtime </tr></thead> |
|
138
|
@ <tbody> |
|
139
|
while( db_step(&q)==SQLITE_ROW ){ |
|
140
|
const char *zTarget = db_column_text(&q, 0); |
|
141
|
int srctype = db_column_int(&q, 1); |
|
142
|
int srcid = db_column_int(&q, 2); |
|
143
|
const char *zMtime = db_column_text(&q, 3); |
|
144
|
@ <tr><td><a href="%R/info/%h(zTarget)">%h(zTarget)</a> |
|
145
|
switch( srctype ){ |
|
146
|
case BKLNK_COMMENT: { |
|
147
|
@ <td><a href="%R/info?name=rid:%d(srcid)">checkin-%d(srcid)</a> |
|
148
|
break; |
|
149
|
} |
|
150
|
case BKLNK_TICKET: { |
|
151
|
@ <td><a href="%R/info?name=rid:%d(srcid)">ticket-%d(srcid)</a> |
|
152
|
break; |
|
153
|
} |
|
154
|
case BKLNK_WIKI: { |
|
155
|
const char *zName = db_column_text(&q, 4); |
|
156
|
@ <td><a href="%R/wiki?name=%h(zName)&p">wiki-%d(srcid)</a> |
|
157
|
break; |
|
158
|
} |
|
159
|
case BKLNK_EVENT: { |
|
160
|
@ <td><a href="%R/info?name=rid:%d(srcid)">tecknote-%d(srcid)</a> |
|
161
|
break; |
|
162
|
} |
|
163
|
case BKLNK_FORUM: { |
|
164
|
@ <td><a href="%R/info?name=rid:%d(srcid)">forum-%d(srcid)</a> |
|
165
|
break; |
|
166
|
} |
|
167
|
default: { |
|
168
|
@ <td>unknown(%d(srctype)) - %d(srcid) |
|
169
|
break; |
|
170
|
} |
|
171
|
} |
|
172
|
@ <td>%h(zMtime)</tr> |
|
173
|
} |
|
174
|
@ </tbody> |
|
175
|
@ </table> |
|
176
|
db_finalize(&q); |
|
177
|
style_finish_page(); |
|
178
|
} |
|
179
|
|
|
180
|
/* |
|
181
|
** Remove all prior backlinks for the wiki page given. Then |
|
182
|
** add new backlinks for the latest version of the wiki page. |
|
183
|
*/ |
|
184
|
void backlink_wiki_refresh(const char *zWikiTitle){ |
|
185
|
int tagid = wiki_tagid(zWikiTitle); |
|
186
|
int rid; |
|
187
|
Manifest *pWiki; |
|
188
|
if( tagid==0 ) return; |
|
189
|
rid = db_int(0, "SELECT rid FROM tagxref WHERE tagid=%d" |
|
190
|
" ORDER BY mtime DESC LIMIT 1", tagid); |
|
191
|
if( rid==0 ) return; |
|
192
|
pWiki = manifest_get(rid, CFTYPE_WIKI, 0); |
|
193
|
if( pWiki ){ |
|
194
|
int mimetype = parse_mimetype( pWiki->zMimetype ); |
|
195
|
backlink_extract(pWiki->zWiki, mimetype, tagid, BKLNK_WIKI, |
|
196
|
pWiki->rDate, 1); |
|
197
|
manifest_destroy(pWiki); |
|
198
|
} |
|
199
|
} |
|
200
|
|
|
201
|
/* |
|
202
|
** Structure used to pass down state information through the |
|
203
|
** markup formatters into the BACKLINK generator. |
|
204
|
*/ |
|
205
|
#if INTERFACE |
|
206
|
struct Backlink { |
|
207
|
int srcid; /* srcid for the source document */ |
|
208
|
int srctype; /* One of BKLNK_*. 0=comment 1=ticket 2=wiki */ |
|
209
|
double mtime; /* mtime field for new BACKLINK table entries */ |
|
210
|
}; |
|
211
|
#endif |
|
212
|
|
|
213
|
|
|
214
|
/* |
|
215
|
** zTarget is a hyperlink target in some markup format. If this |
|
216
|
** target is a self-reference to some other object in the repository, |
|
217
|
** then create an appropriate backlink. |
|
218
|
*/ |
|
219
|
void backlink_create(Backlink *p, const char *zTarget, int nTarget){ |
|
220
|
char zLink[HNAME_MAX+4]; |
|
221
|
if( zTarget==0 ) return; |
|
222
|
if( nTarget<4 ) return; |
|
223
|
if( nTarget>=10 && strncmp(zTarget,"/info/",6)==0 ){ |
|
224
|
zTarget += 6; |
|
225
|
nTarget -= 6; |
|
226
|
} |
|
227
|
if( nTarget>HNAME_MAX ) return; |
|
228
|
if( !validate16(zTarget, nTarget) ) return; |
|
229
|
memcpy(zLink, zTarget, nTarget); |
|
230
|
zLink[nTarget] = 0; |
|
231
|
canonical16(zLink, nTarget); |
|
232
|
db_multi_exec( |
|
233
|
"REPLACE INTO backlink(target,srctype,srcid,mtime)" |
|
234
|
"VALUES(%Q,%d,%d,%.17g)", zLink, p->srctype, p->srcid, p->mtime |
|
235
|
); |
|
236
|
} |
|
237
|
|
|
238
|
/* |
|
239
|
** This routine is called by the markdown formatter for each hyperlink. |
|
240
|
** If the hyperlink is a backlink, add it to the BACKLINK table. |
|
241
|
*/ |
|
242
|
static int backlink_md_link( |
|
243
|
Blob *ob, /* Write output text here (not used in this case) */ |
|
244
|
Blob *target, /* The hyperlink target */ |
|
245
|
Blob *title, /* Hyperlink title */ |
|
246
|
Blob *content, /* Content of the link */ |
|
247
|
void *opaque |
|
248
|
){ |
|
249
|
Backlink *p = (Backlink*)opaque; |
|
250
|
char *zTarget = blob_buffer(target); |
|
251
|
int nTarget = blob_size(target); |
|
252
|
|
|
253
|
backlink_create(p, zTarget, nTarget); |
|
254
|
return 1; |
|
255
|
} |
|
256
|
|
|
257
|
/* No-op routines for the rendering callbacks that we do not need */ |
|
258
|
static void mkdn_noop_prolog(Blob *b, void *v){ return; } |
|
259
|
static void (*mkdn_noop_epilog)(Blob*, void*) = mkdn_noop_prolog; |
|
260
|
static void mkdn_noop_footnotes(Blob *b1, const Blob *b2, void *v){ return; } |
|
261
|
static void mkdn_noop_blockcode(Blob *b1, Blob *b2, void *v){ return; } |
|
262
|
static void (*mkdn_noop_blockquote)(Blob*, Blob*, void*) = mkdn_noop_blockcode; |
|
263
|
static void (*mkdn_noop_blockhtml)(Blob*, Blob*, void*) = mkdn_noop_blockcode; |
|
264
|
static void mkdn_noop_header(Blob *b1, Blob *b2, int i, void *v){ return; } |
|
265
|
static void (*mkdn_noop_hrule)(Blob*, void*) = mkdn_noop_prolog; |
|
266
|
static void (*mkdn_noop_list)(Blob*, Blob*, int, void*) = mkdn_noop_header; |
|
267
|
static void (*mkdn_noop_listitem)(Blob*, Blob*, int, void*) = mkdn_noop_header; |
|
268
|
static void (*mkdn_noop_paragraph)(Blob*, Blob*, void*) = mkdn_noop_blockcode; |
|
269
|
static void mkdn_noop_table(Blob *b1, Blob *b2, Blob *b3, void *v){ return; } |
|
270
|
static void (*mkdn_noop_table_cell)(Blob*, Blob*, int, |
|
271
|
void*) = mkdn_noop_header; |
|
272
|
static void (*mkdn_noop_table_row)(Blob*, Blob*, int, |
|
273
|
void*) = mkdn_noop_header; |
|
274
|
static void mkdn_noop_footnoteitm(Blob *b1, const Blob *b2, int i1, int i2, |
|
275
|
void *v){ return; } |
|
276
|
static int mkdn_noop_autolink(Blob *b1, Blob *b2, enum mkd_autolink e, |
|
277
|
void *v){ return 1; } |
|
278
|
static int mkdn_noop_codespan(Blob *b1, Blob *b2, int i, void *v){ return 1; } |
|
279
|
static int mkdn_noop_emphasis(Blob *b1, Blob *b2, char c, void *v){ return 1; } |
|
280
|
static int (*mkdn_noop_dbl_emphas)(Blob*, Blob*, char, |
|
281
|
void*) = mkdn_noop_emphasis; |
|
282
|
static int mkdn_noop_image(Blob *b1, Blob *b2, Blob *b3, Blob *b4, |
|
283
|
void *v){ return 1; } |
|
284
|
static int mkdn_noop_linebreak(Blob *b1, void *v){ return 1; } |
|
285
|
static int mkdn_noop_r_html_tag(Blob *b1, Blob *b2, void *v){ return 1; } |
|
286
|
static int (*mkdn_noop_tri_emphas)(Blob*, Blob*, char, |
|
287
|
void*) = mkdn_noop_emphasis; |
|
288
|
static int mkdn_noop_footnoteref(Blob *b1, const Blob *b2, const Blob *b3, |
|
289
|
int i1, int i2, void *v){ return 1; } |
|
290
|
|
|
291
|
/* |
|
292
|
** Scan markdown text and add self-hyperlinks to the BACKLINK table. |
|
293
|
*/ |
|
294
|
void markdown_extract_links( |
|
295
|
char *zInputText, |
|
296
|
Backlink *p |
|
297
|
){ |
|
298
|
struct mkd_renderer html_renderer = { |
|
299
|
/* prolog */ mkdn_noop_prolog, |
|
300
|
/* epilog */ mkdn_noop_epilog, |
|
301
|
/* footnotes */ mkdn_noop_footnotes, |
|
302
|
|
|
303
|
/* blockcode */ mkdn_noop_blockcode, |
|
304
|
/* blockquote */ mkdn_noop_blockquote, |
|
305
|
/* blockhtml */ mkdn_noop_blockhtml, |
|
306
|
/* header */ mkdn_noop_header, |
|
307
|
/* hrule */ mkdn_noop_hrule, |
|
308
|
/* list */ mkdn_noop_list, |
|
309
|
/* listitem */ mkdn_noop_listitem, |
|
310
|
/* paragraph */ mkdn_noop_paragraph, |
|
311
|
/* table */ mkdn_noop_table, |
|
312
|
/* table_cell */ mkdn_noop_table_cell, |
|
313
|
/* table_row */ mkdn_noop_table_row, |
|
314
|
/* footnoteitm*/ mkdn_noop_footnoteitm, |
|
315
|
|
|
316
|
/* autolink */ mkdn_noop_autolink, |
|
317
|
/* codespan */ mkdn_noop_codespan, |
|
318
|
/* dbl_emphas */ mkdn_noop_dbl_emphas, |
|
319
|
/* emphasis */ mkdn_noop_emphasis, |
|
320
|
/* image */ mkdn_noop_image, |
|
321
|
/* linebreak */ mkdn_noop_linebreak, |
|
322
|
/* link */ backlink_md_link, |
|
323
|
/* r_html_tag */ mkdn_noop_r_html_tag, |
|
324
|
/* tri_emphas */ mkdn_noop_tri_emphas, |
|
325
|
/* footnoteref*/ mkdn_noop_footnoteref, |
|
326
|
|
|
327
|
0, /* entity */ |
|
328
|
0, /* normal_text */ |
|
329
|
"*_", /* emphasis characters */ |
|
330
|
0 /* client data */ |
|
331
|
}; |
|
332
|
Blob out, in; |
|
333
|
html_renderer.opaque = (void*)p; |
|
334
|
blob_init(&out, 0, 0); |
|
335
|
blob_init(&in, zInputText, -1); |
|
336
|
markdown(&out, &in, &html_renderer); |
|
337
|
blob_reset(&out); |
|
338
|
blob_reset(&in); |
|
339
|
} |
|
340
|
|
|
341
|
/* |
|
342
|
** Transform mimetype string into an integer code. |
|
343
|
** NOTE: In the sake of compatibility empty string is parsed as MT_UNKNOWN; |
|
344
|
** it is yet unclear whether it can safely be changed to MT_NONE. |
|
345
|
*/ |
|
346
|
int parse_mimetype(const char* zMimetype){ |
|
347
|
if( zMimetype==0 ) return MT_NONE; |
|
348
|
if( strstr(zMimetype,"wiki")!=0 ) return MT_WIKI; |
|
349
|
if( strstr(zMimetype,"markdown")!=0 ) return MT_MARKDOWN; |
|
350
|
return MT_UNKNOWN; |
|
351
|
} |
|
352
|
/* |
|
353
|
** Parse text looking for hyperlinks. Insert references into the |
|
354
|
** BACKLINK table. |
|
355
|
*/ |
|
356
|
void backlink_extract( |
|
357
|
char *zSrc, /* Input text from which links are extracted */ |
|
358
|
int mimetype, /* Mimetype of input. MT_NONE works as MT_WIKI */ |
|
359
|
int srcid, /* srcid for the source document */ |
|
360
|
int srctype, /* One of BKLNK_*. 0=comment 1=ticket 2=wiki */ |
|
361
|
double mtime, /* mtime field for new BACKLINK table entries */ |
|
362
|
int replaceFlag /* True to overwrite prior BACKLINK entries */ |
|
363
|
){ |
|
364
|
Backlink bklnk; |
|
365
|
if( replaceFlag ){ |
|
366
|
db_multi_exec("DELETE FROM backlink WHERE srctype=%d AND srcid=%d", |
|
367
|
srctype, srcid); |
|
368
|
} |
|
369
|
bklnk.srcid = srcid; |
|
370
|
assert( ValidBklnk(srctype) ); |
|
371
|
assert( ValidMTC(mimetype) ); |
|
372
|
bklnk.srctype = srctype; |
|
373
|
bklnk.mtime = mtime; |
|
374
|
if( mimetype==MT_NONE || mimetype==MT_WIKI ){ |
|
375
|
wiki_extract_links(zSrc, &bklnk, srctype==BKLNK_COMMENT ? WIKI_INLINE : 0); |
|
376
|
}else if( mimetype==MT_MARKDOWN ){ |
|
377
|
markdown_extract_links(zSrc, &bklnk); |
|
378
|
} |
|
379
|
} |
|
380
|
|
|
381
|
/* |
|
382
|
** COMMAND: test-backlinks |
|
383
|
** |
|
384
|
** Usage: %fossil test-backlinks SRCTYPE SRCID ?OPTIONS? INPUT-FILE |
|
385
|
** |
|
386
|
** Read the content of INPUT-FILE and pass it into the backlink_extract() |
|
387
|
** routine. But instead of adding backlinks to the backlink table, |
|
388
|
** just print them on stdout. SRCID and SRCTYPE are integers. |
|
389
|
** |
|
390
|
** Options: |
|
391
|
** --mtime DATETIME Use an alternative date/time. Defaults to the |
|
392
|
** current date/time. |
|
393
|
** --mimetype TYPE Use an alternative mimetype |
|
394
|
*/ |
|
395
|
void test_backlinks_cmd(void){ |
|
396
|
const char *zMTime = find_option("mtime",0,1); |
|
397
|
const char *zMimetype = find_option("mimetype",0,1); |
|
398
|
const int mimetype = parse_mimetype(zMimetype); |
|
399
|
Blob in; |
|
400
|
int srcid; |
|
401
|
int srctype; |
|
402
|
double mtime; |
|
403
|
|
|
404
|
verify_all_options(); |
|
405
|
if( g.argc!=5 ){ |
|
406
|
usage("SRCTYPE SRCID INPUTFILE"); |
|
407
|
} |
|
408
|
srctype = atoi(g.argv[2]); |
|
409
|
if( srctype<0 || srctype>2 ){ |
|
410
|
fossil_fatal("SRCTYPE should be an integer 0, 1, or 2"); |
|
411
|
} |
|
412
|
srcid = atoi(g.argv[3]); |
|
413
|
blob_read_from_file(&in, g.argv[4], ExtFILE); |
|
414
|
sqlite3_open(":memory:",&g.db); |
|
415
|
if( zMTime==0 ) zMTime = "now"; |
|
416
|
mtime = db_double(1721059.5,"SELECT julianday(%Q)",zMTime); |
|
417
|
g.fSqlPrint = 1; |
|
418
|
sqlite3_create_function(g.db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0); |
|
419
|
db_multi_exec( |
|
420
|
"CREATE TEMP TABLE backlink(target,srctype,srcid,mtime);\n" |
|
421
|
"CREATE TRIGGER backlink_insert BEFORE INSERT ON backlink BEGIN\n" |
|
422
|
" SELECT print(" |
|
423
|
" 'target='||quote(new.target)||" |
|
424
|
" ' srctype='||quote(new.srctype)||" |
|
425
|
" ' srcid='||quote(new.srcid)||" |
|
426
|
" ' mtime='||datetime(new.mtime));\n" |
|
427
|
" SELECT raise(ignore);\n" |
|
428
|
"END;" |
|
429
|
); |
|
430
|
backlink_extract(blob_str(&in),mimetype,srcid,srctype,mtime,0); |
|
431
|
blob_reset(&in); |
|
432
|
} |
|
433
|
|
|
434
|
|
|
435
|
/* |
|
436
|
** COMMAND: test-relink-wiki |
|
437
|
** |
|
438
|
** Usage: %fossil test-relink-wiki WIKI-PAGE-NAME |
|
439
|
** |
|
440
|
** Run the backlink_wiki_refresh() procedure on the wiki page |
|
441
|
** named. WIKI-PAGE-NAME can be a glob pattern or a prefix |
|
442
|
** of the wiki page. |
|
443
|
*/ |
|
444
|
void test_wiki_relink_cmd(void){ |
|
445
|
Stmt q; |
|
446
|
db_find_and_open_repository(0, 0); |
|
447
|
if( g.argc!=3 ) usage("WIKI-PAGE-NAME"); |
|
448
|
db_prepare(&q, |
|
449
|
"SELECT substr(tagname,6) FROM tag WHERE tagname GLOB 'wiki-%q*'", |
|
450
|
g.argv[2] |
|
451
|
); |
|
452
|
while( db_step(&q)==SQLITE_ROW ){ |
|
453
|
const char *zPage = db_column_text(&q,0); |
|
454
|
fossil_print("Relinking page: %s\n", zPage); |
|
455
|
backlink_wiki_refresh(zPage); |
|
456
|
} |
|
457
|
db_finalize(&q); |
|
458
|
} |
|
459
|
|