Fossil SCM

Added #NNN and #NNN.NNN references as a special case of hashtag, noting that it will currently match a prefix of #NNN.NNN.NNN. Taught /chat that clicking on such a reference should jump to the referenced message or toast the user that the message is not in the current history.

stephan 2021-09-25 12:26 markdown-tagrefs
Commit 4539bf8792545485b98fed31f22fd0d76e9f610a77d72071f015b080f85e5caf
+31 -4
--- src/chat.js
+++ src/chat.js
@@ -175,11 +175,11 @@
175175
match: function(tag){
176176
return !this.activeTag || tag===this.activeTag;
177177
},
178178
matchElem: function(e){
179179
return !this.activeTag
180
- || !!e.querySelector('[data-hashtag='+this.activeTag+']');
180
+ || !!e.querySelector('[data-hashtag="'+this.activeTag+'"]');
181181
}
182182
},
183183
current: undefined/*gets set to current active filter*/
184184
},
185185
/** Gets (no args) or sets (1 arg) the current input text field value,
@@ -895,24 +895,42 @@
895895
after initial processing of the message, to set up
896896
hashtag references. */
897897
const setupHashtags = function f(elem){
898898
if(!f.$click){
899899
f.$click = function(ev){
900
+ /* Click handler for hashtags */
900901
const tag = ev.target.dataset.hashtag;
901902
if(tag){
902903
Chat.setHashtagFilter(
903904
tag===Chat.filter.hashtag.activeTag
904905
? false : tag
905906
);
906907
}
907908
};
909
+ f.$clickNum = function(ev){
910
+ /* Click handler for #NNN references */
911
+ const tag = ev.target.dataset.numtag;
912
+ if(tag){
913
+ const e = Chat.e.viewMessages.querySelector(
914
+ '.message-widget[data-msgid="'+tag+'"]'
915
+ );
916
+ if(e){
917
+ Chat.MessageWidget.scrollToMessageElem(e);
918
+ }else{
919
+ F.toast.warning("Message #"+tag+" not found in loaded messages.");
920
+ }
921
+ }
922
+ };
908923
}
909924
elem.querySelectorAll('[data-hashtag]').forEach(function(e){
910925
e.dataset.hashtag = e.dataset.hashtag.toLowerCase();
911926
e.addEventListener('click', f.$click, false);
912927
})
913
- };
928
+ elem.querySelectorAll('[data-numtag]').forEach(
929
+ (e)=>e.addEventListener('click', f.$clickNum, false)
930
+ )
931
+ }/*setupHashtags()*/;
914932
915933
/**
916934
Custom widget type for rendering messages (one message per
917935
instance). These are modelled after FIELDSET elements but we
918936
don't use FIELDSET because of cross-browser inconsistencies in
@@ -1128,12 +1146,11 @@
11281146
D.button(
11291147
"Message in context",
11301148
function(){
11311149
self.hide();
11321150
Chat.clearFilters();
1133
- eMsg.scrollIntoView(false);
1134
- Chat.animate(eMsg.firstElementChild, 'anim-flip-h');
1151
+ Chat.MessageWidget.scrollToMessageElem(eMsg);
11351152
})
11361153
)
11371154
);
11381155
}/*jump-to button*/
11391156
}
@@ -1161,10 +1178,20 @@
11611178
while( theMsg && !theMsg.classList.contains('message-widget')){
11621179
theMsg = theMsg.parentNode;
11631180
}
11641181
if(theMsg) f.popup.show(theMsg);
11651182
}/*_handleLegendClicked()*/
1183
+ }/*MessageWidget.prototype*/;
1184
+ /** Assumes that e is a MessageWidget element, ensures that
1185
+ Chat.e.viewMessages is visible, scrolls the message,
1186
+ and animates it a bit to make it more visible. */
1187
+ cf.scrollToMessageElem = function(e){
1188
+ if(e.firstElementChild){
1189
+ Chat.setCurrentView(Chat.e.viewMessages);
1190
+ e.scrollIntoView(false);
1191
+ Chat.animate(e.firstElementChild, 'anim-flip-h');
1192
+ }
11661193
};
11671194
return cf;
11681195
})()/*MessageWidget*/;
11691196
11701197
const BlobXferState = (function(){/*drag/drop bits...*/
11711198
--- src/chat.js
+++ src/chat.js
@@ -175,11 +175,11 @@
175 match: function(tag){
176 return !this.activeTag || tag===this.activeTag;
177 },
178 matchElem: function(e){
179 return !this.activeTag
180 || !!e.querySelector('[data-hashtag='+this.activeTag+']');
181 }
182 },
183 current: undefined/*gets set to current active filter*/
184 },
185 /** Gets (no args) or sets (1 arg) the current input text field value,
@@ -895,24 +895,42 @@
895 after initial processing of the message, to set up
896 hashtag references. */
897 const setupHashtags = function f(elem){
898 if(!f.$click){
899 f.$click = function(ev){
 
900 const tag = ev.target.dataset.hashtag;
901 if(tag){
902 Chat.setHashtagFilter(
903 tag===Chat.filter.hashtag.activeTag
904 ? false : tag
905 );
906 }
907 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908 }
909 elem.querySelectorAll('[data-hashtag]').forEach(function(e){
910 e.dataset.hashtag = e.dataset.hashtag.toLowerCase();
911 e.addEventListener('click', f.$click, false);
912 })
913 };
 
 
 
914
915 /**
916 Custom widget type for rendering messages (one message per
917 instance). These are modelled after FIELDSET elements but we
918 don't use FIELDSET because of cross-browser inconsistencies in
@@ -1128,12 +1146,11 @@
1128 D.button(
1129 "Message in context",
1130 function(){
1131 self.hide();
1132 Chat.clearFilters();
1133 eMsg.scrollIntoView(false);
1134 Chat.animate(eMsg.firstElementChild, 'anim-flip-h');
1135 })
1136 )
1137 );
1138 }/*jump-to button*/
1139 }
@@ -1161,10 +1178,20 @@
1161 while( theMsg && !theMsg.classList.contains('message-widget')){
1162 theMsg = theMsg.parentNode;
1163 }
1164 if(theMsg) f.popup.show(theMsg);
1165 }/*_handleLegendClicked()*/
 
 
 
 
 
 
 
 
 
 
1166 };
1167 return cf;
1168 })()/*MessageWidget*/;
1169
1170 const BlobXferState = (function(){/*drag/drop bits...*/
1171
--- src/chat.js
+++ src/chat.js
@@ -175,11 +175,11 @@
175 match: function(tag){
176 return !this.activeTag || tag===this.activeTag;
177 },
178 matchElem: function(e){
179 return !this.activeTag
180 || !!e.querySelector('[data-hashtag="'+this.activeTag+'"]');
181 }
182 },
183 current: undefined/*gets set to current active filter*/
184 },
185 /** Gets (no args) or sets (1 arg) the current input text field value,
@@ -895,24 +895,42 @@
895 after initial processing of the message, to set up
896 hashtag references. */
897 const setupHashtags = function f(elem){
898 if(!f.$click){
899 f.$click = function(ev){
900 /* Click handler for hashtags */
901 const tag = ev.target.dataset.hashtag;
902 if(tag){
903 Chat.setHashtagFilter(
904 tag===Chat.filter.hashtag.activeTag
905 ? false : tag
906 );
907 }
908 };
909 f.$clickNum = function(ev){
910 /* Click handler for #NNN references */
911 const tag = ev.target.dataset.numtag;
912 if(tag){
913 const e = Chat.e.viewMessages.querySelector(
914 '.message-widget[data-msgid="'+tag+'"]'
915 );
916 if(e){
917 Chat.MessageWidget.scrollToMessageElem(e);
918 }else{
919 F.toast.warning("Message #"+tag+" not found in loaded messages.");
920 }
921 }
922 };
923 }
924 elem.querySelectorAll('[data-hashtag]').forEach(function(e){
925 e.dataset.hashtag = e.dataset.hashtag.toLowerCase();
926 e.addEventListener('click', f.$click, false);
927 })
928 elem.querySelectorAll('[data-numtag]').forEach(
929 (e)=>e.addEventListener('click', f.$clickNum, false)
930 )
931 }/*setupHashtags()*/;
932
933 /**
934 Custom widget type for rendering messages (one message per
935 instance). These are modelled after FIELDSET elements but we
936 don't use FIELDSET because of cross-browser inconsistencies in
@@ -1128,12 +1146,11 @@
1146 D.button(
1147 "Message in context",
1148 function(){
1149 self.hide();
1150 Chat.clearFilters();
1151 Chat.MessageWidget.scrollToMessageElem(eMsg);
 
1152 })
1153 )
1154 );
1155 }/*jump-to button*/
1156 }
@@ -1161,10 +1178,20 @@
1178 while( theMsg && !theMsg.classList.contains('message-widget')){
1179 theMsg = theMsg.parentNode;
1180 }
1181 if(theMsg) f.popup.show(theMsg);
1182 }/*_handleLegendClicked()*/
1183 }/*MessageWidget.prototype*/;
1184 /** Assumes that e is a MessageWidget element, ensures that
1185 Chat.e.viewMessages is visible, scrolls the message,
1186 and animates it a bit to make it more visible. */
1187 cf.scrollToMessageElem = function(e){
1188 if(e.firstElementChild){
1189 Chat.setCurrentView(Chat.e.viewMessages);
1190 e.scrollIntoView(false);
1191 Chat.animate(e.firstElementChild, 'anim-flip-h');
1192 }
1193 };
1194 return cf;
1195 })()/*MessageWidget*/;
1196
1197 const BlobXferState = (function(){/*drag/drop bits...*/
1198
+46 -18
--- src/markdown.c
+++ src/markdown.c
@@ -954,62 +954,90 @@
954954
size_t offset,
955955
size_t size
956956
){
957957
size_t end;
958958
struct Blob work = BLOB_INITIALIZER;
959
- int nUscore = 0;
959
+ int nUscore = 0; /* Consecutive underscore counter */;
960
+ int numberMode = 0 /* 0 for normal, 1 for #NNN numeric,
961
+ and 2 for #NNN.NNN. */;
960962
if(offset>0 && !fossil_isspace(data[-1])){
961
- /* Only ever match if the *previous* character is
962
- whitespace or we're at the start of the input.
963
- Note that we rely on fossil processing emphasis
964
- markup before reaching this function, so *#Hash*
965
- will Do The Right Thing. */
963
+ /* Only ever match if the *previous* character is whitespace or
964
+ we're at the start of the input. Note that we rely on fossil
965
+ processing emphasis markup before reaching this function, so
966
+ *#Hash* will Do The Right Thing. Not that this means that
967
+ "#Hash." will match while ".#Hash" won't. That's okay. */
966968
return 0;
967969
}
968970
assert( '#' == data[0] );
969
- if(size < 2 || !fossil_isalpha(data[1])) return 0;
971
+ if(size < 2) return 0;
972
+ if(fossil_isdigit(data[1])){
973
+ numberMode = 1;
974
+ }else if(!fossil_isalpha(data[1])){
975
+ return 0;
976
+ }
970977
#if 0
971978
fprintf(stderr,"HASHREF offset=%d size=%d: %.*s\n",
972979
(int)offset, (int)size, (int)size, data);
973980
#endif
974981
#define HASHTAG_LEGAL_END \
975
- case ' ': case '\t': case '\r': case '\n': case '.': case ':': case ';': case '!': case '?'
982
+ case ' ': case '\t': case '\r': case '\n': \
983
+ case ':': case ';': case '!': case '?': case ','
984
+ /* ^^^^ '.' is handled separately */
976985
for(end = 2; end < size; ++end){
977986
char ch = data[end];
987
+ /* Potential TODO: if (ch & 0xF0), treat it as valid, skip that
988
+ multi-byte character's length characters, and continue
989
+ looping. Reminder: UTF8 char lengths can be determined by
990
+ masking against 0xF0: 0xf0==4, 0xe0==3, 0xc0==2, else 1. */
978991
switch(ch){
979992
case '_':
980993
/* Multiple adjacent underscores not permitted. */
981
- if(++nUscore>1) goto hashref_bailout;
994
+ if(numberMode>0 || ++nUscore>1) goto hashref_bailout;
995
+ break;
996
+ case '.':
997
+ if(1==numberMode) ++numberMode;
998
+ ch = 0;
982999
break;
9831000
HASHTAG_LEGAL_END:
984
- if(end<3) goto hashref_bailout/*require 2+ characters (arbitrary)*/;
1001
+ if(numberMode==0 && end<3){
1002
+ goto hashref_bailout/*require 2+ characters (arbitrary)*/;
1003
+ }
9851004
ch = 0;
9861005
break;
1006
+ case '0': case '1': case '2': case '3': case '4':
1007
+ case '5': case '6': case '7': case '8': case '9':
1008
+ break;
9871009
default:
988
- if(!fossil_isalnum(ch)) goto hashref_bailout;
1010
+ if(numberMode!=0 || !fossil_isalpha(ch)){
1011
+ goto hashref_bailout;
1012
+ }
9891013
nUscore = 0;
9901014
break;
9911015
}
9921016
if(ch) continue;
993
- else break;
1017
+ break;
1018
+ }
1019
+ if(numberMode>1){
1020
+ /* Check for trailing part of #NNN.nnn... */
1021
+ assert('.'==data[end]);
1022
+ if(end<size-1 && fossil_isdigit(data[end+1])){
1023
+ for(++end; end<size; ++end){
1024
+ if(!fossil_isdigit(data[end])) break;
1025
+ }
1026
+ }
9941027
}
9951028
#if 0
9961029
fprintf(stderr,"?HASHREF length=%d: %.*s\n",
9971030
(int)end, (int)end, data);
9981031
#endif
999
- /*TODO: in order to support detection of forum post-style
1000
- references, we need to recognize #X.Y, but only when X and Y are
1001
- both purely numeric and Y ends on a word/sentence
1002
- boundary.*/
10031032
if(end<size){
10041033
/* Only match if we end at end of input or what "might" be the end
10051034
of a natural language grammar construct, e.g. period or
10061035
[semi]colon. */
10071036
switch(data[end]){
1037
+ case '.':
10081038
HASHTAG_LEGAL_END:
1009
- /* We could arguably treat any leading multi-byte character as
1010
- valid here. */
10111039
break;
10121040
default:
10131041
goto hashref_bailout;
10141042
}
10151043
}
10161044
--- src/markdown.c
+++ src/markdown.c
@@ -954,62 +954,90 @@
954 size_t offset,
955 size_t size
956 ){
957 size_t end;
958 struct Blob work = BLOB_INITIALIZER;
959 int nUscore = 0;
 
 
960 if(offset>0 && !fossil_isspace(data[-1])){
961 /* Only ever match if the *previous* character is
962 whitespace or we're at the start of the input.
963 Note that we rely on fossil processing emphasis
964 markup before reaching this function, so *#Hash*
965 will Do The Right Thing. */
966 return 0;
967 }
968 assert( '#' == data[0] );
969 if(size < 2 || !fossil_isalpha(data[1])) return 0;
 
 
 
 
 
970 #if 0
971 fprintf(stderr,"HASHREF offset=%d size=%d: %.*s\n",
972 (int)offset, (int)size, (int)size, data);
973 #endif
974 #define HASHTAG_LEGAL_END \
975 case ' ': case '\t': case '\r': case '\n': case '.': case ':': case ';': case '!': case '?'
 
 
976 for(end = 2; end < size; ++end){
977 char ch = data[end];
 
 
 
 
978 switch(ch){
979 case '_':
980 /* Multiple adjacent underscores not permitted. */
981 if(++nUscore>1) goto hashref_bailout;
 
 
 
 
982 break;
983 HASHTAG_LEGAL_END:
984 if(end<3) goto hashref_bailout/*require 2+ characters (arbitrary)*/;
 
 
985 ch = 0;
986 break;
 
 
 
987 default:
988 if(!fossil_isalnum(ch)) goto hashref_bailout;
 
 
989 nUscore = 0;
990 break;
991 }
992 if(ch) continue;
993 else break;
 
 
 
 
 
 
 
 
 
994 }
995 #if 0
996 fprintf(stderr,"?HASHREF length=%d: %.*s\n",
997 (int)end, (int)end, data);
998 #endif
999 /*TODO: in order to support detection of forum post-style
1000 references, we need to recognize #X.Y, but only when X and Y are
1001 both purely numeric and Y ends on a word/sentence
1002 boundary.*/
1003 if(end<size){
1004 /* Only match if we end at end of input or what "might" be the end
1005 of a natural language grammar construct, e.g. period or
1006 [semi]colon. */
1007 switch(data[end]){
 
1008 HASHTAG_LEGAL_END:
1009 /* We could arguably treat any leading multi-byte character as
1010 valid here. */
1011 break;
1012 default:
1013 goto hashref_bailout;
1014 }
1015 }
1016
--- src/markdown.c
+++ src/markdown.c
@@ -954,62 +954,90 @@
954 size_t offset,
955 size_t size
956 ){
957 size_t end;
958 struct Blob work = BLOB_INITIALIZER;
959 int nUscore = 0; /* Consecutive underscore counter */;
960 int numberMode = 0 /* 0 for normal, 1 for #NNN numeric,
961 and 2 for #NNN.NNN. */;
962 if(offset>0 && !fossil_isspace(data[-1])){
963 /* Only ever match if the *previous* character is whitespace or
964 we're at the start of the input. Note that we rely on fossil
965 processing emphasis markup before reaching this function, so
966 *#Hash* will Do The Right Thing. Not that this means that
967 "#Hash." will match while ".#Hash" won't. That's okay. */
968 return 0;
969 }
970 assert( '#' == data[0] );
971 if(size < 2) return 0;
972 if(fossil_isdigit(data[1])){
973 numberMode = 1;
974 }else if(!fossil_isalpha(data[1])){
975 return 0;
976 }
977 #if 0
978 fprintf(stderr,"HASHREF offset=%d size=%d: %.*s\n",
979 (int)offset, (int)size, (int)size, data);
980 #endif
981 #define HASHTAG_LEGAL_END \
982 case ' ': case '\t': case '\r': case '\n': \
983 case ':': case ';': case '!': case '?': case ','
984 /* ^^^^ '.' is handled separately */
985 for(end = 2; end < size; ++end){
986 char ch = data[end];
987 /* Potential TODO: if (ch & 0xF0), treat it as valid, skip that
988 multi-byte character's length characters, and continue
989 looping. Reminder: UTF8 char lengths can be determined by
990 masking against 0xF0: 0xf0==4, 0xe0==3, 0xc0==2, else 1. */
991 switch(ch){
992 case '_':
993 /* Multiple adjacent underscores not permitted. */
994 if(numberMode>0 || ++nUscore>1) goto hashref_bailout;
995 break;
996 case '.':
997 if(1==numberMode) ++numberMode;
998 ch = 0;
999 break;
1000 HASHTAG_LEGAL_END:
1001 if(numberMode==0 && end<3){
1002 goto hashref_bailout/*require 2+ characters (arbitrary)*/;
1003 }
1004 ch = 0;
1005 break;
1006 case '0': case '1': case '2': case '3': case '4':
1007 case '5': case '6': case '7': case '8': case '9':
1008 break;
1009 default:
1010 if(numberMode!=0 || !fossil_isalpha(ch)){
1011 goto hashref_bailout;
1012 }
1013 nUscore = 0;
1014 break;
1015 }
1016 if(ch) continue;
1017 break;
1018 }
1019 if(numberMode>1){
1020 /* Check for trailing part of #NNN.nnn... */
1021 assert('.'==data[end]);
1022 if(end<size-1 && fossil_isdigit(data[end+1])){
1023 for(++end; end<size; ++end){
1024 if(!fossil_isdigit(data[end])) break;
1025 }
1026 }
1027 }
1028 #if 0
1029 fprintf(stderr,"?HASHREF length=%d: %.*s\n",
1030 (int)end, (int)end, data);
1031 #endif
 
 
 
 
1032 if(end<size){
1033 /* Only match if we end at end of input or what "might" be the end
1034 of a natural language grammar construct, e.g. period or
1035 [semi]colon. */
1036 switch(data[end]){
1037 case '.':
1038 HASHTAG_LEGAL_END:
 
 
1039 break;
1040 default:
1041 goto hashref_bailout;
1042 }
1043 }
1044
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -553,11 +553,20 @@
553553
BLOB_APPEND_LITERAL(ob, "<span data-");
554554
switch (type) {
555555
case MKDT_ATREF:
556556
cPrefix = '@'; BLOB_APPEND_LITERAL(ob, "atref"); break;
557557
case MKDT_HASHTAG:
558
- cPrefix = '#'; BLOB_APPEND_LITERAL(ob, "hashtag"); break;
558
+ cPrefix = '#';
559
+ if(fossil_isdigit(*blob_str(text))){
560
+ /* This is a #NNN or #NNN.NNN reference. Mark it differently
561
+ because these will be handled differently by higher-level
562
+ code than conventional hashtags will. */
563
+ BLOB_APPEND_LITERAL(ob, "numtag");
564
+ }else{
565
+ BLOB_APPEND_LITERAL(ob, "hashtag");
566
+ }
567
+ break;
559568
}
560569
BLOB_APPEND_LITERAL(ob, "=\"");
561570
html_quote(ob, blob_buffer(text), blob_size(text));
562571
BLOB_APPEND_LITERAL(ob, "\"");
563572
blob_appendf(ob, ">%c%b</span>", cPrefix,text);
564573
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -553,11 +553,20 @@
553 BLOB_APPEND_LITERAL(ob, "<span data-");
554 switch (type) {
555 case MKDT_ATREF:
556 cPrefix = '@'; BLOB_APPEND_LITERAL(ob, "atref"); break;
557 case MKDT_HASHTAG:
558 cPrefix = '#'; BLOB_APPEND_LITERAL(ob, "hashtag"); break;
 
 
 
 
 
 
 
 
 
559 }
560 BLOB_APPEND_LITERAL(ob, "=\"");
561 html_quote(ob, blob_buffer(text), blob_size(text));
562 BLOB_APPEND_LITERAL(ob, "\"");
563 blob_appendf(ob, ">%c%b</span>", cPrefix,text);
564
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -553,11 +553,20 @@
553 BLOB_APPEND_LITERAL(ob, "<span data-");
554 switch (type) {
555 case MKDT_ATREF:
556 cPrefix = '@'; BLOB_APPEND_LITERAL(ob, "atref"); break;
557 case MKDT_HASHTAG:
558 cPrefix = '#';
559 if(fossil_isdigit(*blob_str(text))){
560 /* This is a #NNN or #NNN.NNN reference. Mark it differently
561 because these will be handled differently by higher-level
562 code than conventional hashtags will. */
563 BLOB_APPEND_LITERAL(ob, "numtag");
564 }else{
565 BLOB_APPEND_LITERAL(ob, "hashtag");
566 }
567 break;
568 }
569 BLOB_APPEND_LITERAL(ob, "=\"");
570 html_quote(ob, blob_buffer(text), blob_size(text));
571 BLOB_APPEND_LITERAL(ob, "\"");
572 blob_appendf(ob, ">%c%b</span>", cPrefix,text);
573
--- src/style.chat.css
+++ src/style.chat.css
@@ -395,11 +395,12 @@
395395
body.chat #chat-user-list .chat-user.selected {
396396
font-weight: bold;
397397
text-decoration: underline;
398398
}
399399
400
-body.chat span[data-hashtag] {
400
+body.chat span[data-hashtag],
401
+body.chat span[data-numtag]{
401402
font-family: monospace;
402403
text-decoration: underline;
403404
cursor: pointer;
404405
}
405406
406407
--- src/style.chat.css
+++ src/style.chat.css
@@ -395,11 +395,12 @@
395 body.chat #chat-user-list .chat-user.selected {
396 font-weight: bold;
397 text-decoration: underline;
398 }
399
400 body.chat span[data-hashtag] {
 
401 font-family: monospace;
402 text-decoration: underline;
403 cursor: pointer;
404 }
405
406
--- src/style.chat.css
+++ src/style.chat.css
@@ -395,11 +395,12 @@
395 body.chat #chat-user-list .chat-user.selected {
396 font-weight: bold;
397 text-decoration: underline;
398 }
399
400 body.chat span[data-hashtag],
401 body.chat span[data-numtag]{
402 font-family: monospace;
403 text-decoration: underline;
404 cursor: pointer;
405 }
406
407

Keyboard Shortcuts

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