mirror of
https://github.com/Jermolene/TiddlyWiki5.git
synced 2026-03-21 14:21:04 -07:00
* Update aho-corasick.js fix transition logic; ensure complete outputs (via failure-output merge); clean up stats/build scoping; clarify CJK boundary behavior. * Update text.js implement global longest-match priority with overlap suppression; fix refresh invalidation to ignore $:/state and drafts; handle deletions precisely to avoid rebuilding on draft deletion; add defensive check for cached automaton presence. * Update text.js remove comment * Update aho-corasick.js remove comment * Create #9672.tid * Create #2026-0222.tid * Delete editions/tw5.com/tiddlers/releasenotes/5.4.0/#2026-0222.tid * Update text.js remove \" * Update and rename #9672.tid to #9676.tid change to right number * Update #9397.tid update the existing release note with the new PR link instead of creating a new release note. * Delete editions/tw5.com/tiddlers/releasenotes/5.4.0/#9676.tid update the existing release note with the new PR link instead of creating a new release note. * Rename #9397.tid to #9676.tid update the existing release note with the new PR link instead of creating a new release note. * Update and rename #9676.tid to #9397.tid add link * Rename #9397.tid to #9676.tid * Update tiddlywiki.info add plugin for test build * Update tiddlywiki.info reverse change, ready to be merge.
279 lines
7.5 KiB
JavaScript
Executable file
279 lines
7.5 KiB
JavaScript
Executable file
/*\
|
|
|
|
title: $:/core/modules/widgets/text.js
|
|
type: application/javascript
|
|
module-type: widget
|
|
|
|
Optimized override of the core text widget that automatically linkifies text.
|
|
- Supports non-Latin languages like Chinese.
|
|
- Global longest-match priority, then removes overlaps.
|
|
- Excludes current tiddler title from linking.
|
|
- Uses Aho-Corasick for performance.
|
|
|
|
\*/
|
|
|
|
"use strict";
|
|
|
|
var TITLE_TARGET_FILTER = "$:/config/Freelinks/TargetFilter";
|
|
var WORD_BOUNDARY_TIDDLER = "$:/config/Freelinks/WordBoundary";
|
|
|
|
var Widget = require("$:/core/modules/widgets/widget.js").widget,
|
|
LinkWidget = require("$:/core/modules/widgets/link.js").link,
|
|
ButtonWidget = require("$:/core/modules/widgets/button.js").button,
|
|
ElementWidget = require("$:/core/modules/widgets/element.js").element,
|
|
AhoCorasick = require("$:/core/modules/utils/aho-corasick.js").AhoCorasick;
|
|
|
|
var TextNodeWidget = function(parseTreeNode,options) {
|
|
this.initialise(parseTreeNode,options);
|
|
};
|
|
|
|
TextNodeWidget.prototype = new Widget();
|
|
|
|
TextNodeWidget.prototype.render = function(parent,nextSibling) {
|
|
this.parentDomNode = parent;
|
|
this.computeAttributes();
|
|
this.execute();
|
|
this.renderChildren(parent,nextSibling);
|
|
};
|
|
|
|
TextNodeWidget.prototype.execute = function() {
|
|
var self = this;
|
|
var ignoreCase = self.getVariable("tv-freelinks-ignore-case",{defaultValue:"no"}).trim() === "yes";
|
|
|
|
var childParseTree = [{
|
|
type: "plain-text",
|
|
text: this.getAttribute("text",this.parseTreeNode.text || "")
|
|
}];
|
|
|
|
var text = childParseTree[0].text;
|
|
if(!text || text.length < 2) {
|
|
this.makeChildWidgets(childParseTree);
|
|
return;
|
|
}
|
|
|
|
if(this.getVariable("tv-wikilinks",{defaultValue:"yes"}) !== "no" &&
|
|
this.getVariable("tv-freelinks",{defaultValue:"no"}) === "yes" &&
|
|
!this.isWithinButtonOrLink()) {
|
|
|
|
var currentTiddlerTitle = this.getVariable("currentTiddler") || "";
|
|
var useWordBoundary = self.wiki.getTiddlerText(WORD_BOUNDARY_TIDDLER,"no") === "yes";
|
|
|
|
var cacheKey = "tiddler-title-info-" + (ignoreCase ? "insensitive" : "sensitive");
|
|
this.tiddlerTitleInfo = this.wiki.getGlobalCache(cacheKey,function() {
|
|
return computeTiddlerTitleInfo(self,ignoreCase);
|
|
});
|
|
|
|
if(this.tiddlerTitleInfo && this.tiddlerTitleInfo.titles && this.tiddlerTitleInfo.titles.length > 0 && this.tiddlerTitleInfo.ac) {
|
|
var newParseTree = this.processTextWithMatches(text,currentTiddlerTitle,ignoreCase,useWordBoundary);
|
|
if(newParseTree && newParseTree.length > 0 &&
|
|
(newParseTree.length > 1 || newParseTree[0].type !== "plain-text")) {
|
|
childParseTree = newParseTree;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.makeChildWidgets(childParseTree);
|
|
};
|
|
|
|
TextNodeWidget.prototype.processTextWithMatches = function(text,currentTiddlerTitle,ignoreCase,useWordBoundary) {
|
|
if(!text || text.length === 0) {
|
|
return [{type: "plain-text", text: text}];
|
|
}
|
|
|
|
var matches;
|
|
try {
|
|
matches = this.tiddlerTitleInfo.ac.search(text, useWordBoundary, ignoreCase);
|
|
} catch(e) {
|
|
return [{type: "plain-text", text: text}];
|
|
}
|
|
|
|
if(!matches || matches.length === 0) {
|
|
return [{type: "plain-text", text: text}];
|
|
}
|
|
|
|
var titleToCompare = ignoreCase ?
|
|
(currentTiddlerTitle ? currentTiddlerTitle.toLowerCase() : "") :
|
|
currentTiddlerTitle;
|
|
|
|
matches.sort(function(a,b) {
|
|
if(b.length !== a.length) return b.length - a.length;
|
|
return a.index - b.index;
|
|
});
|
|
|
|
var occupied = new Uint8Array(text.length);
|
|
var validMatches = [];
|
|
|
|
for(var i = 0; i < matches.length; i++) {
|
|
var m = matches[i];
|
|
var start = m.index;
|
|
var end = start + m.length;
|
|
if(start < 0 || end > text.length) continue;
|
|
|
|
var matchedTitle = this.tiddlerTitleInfo.titles[m.titleIndex];
|
|
if(!matchedTitle) continue;
|
|
|
|
var matchedTitleToCompare = ignoreCase ? matchedTitle.toLowerCase() : matchedTitle;
|
|
if(titleToCompare && matchedTitleToCompare === titleToCompare) continue;
|
|
|
|
var overlapping = false;
|
|
for(var j = start; j < end; j++) {
|
|
if(occupied[j]) { overlapping = true; break; }
|
|
}
|
|
if(overlapping) continue;
|
|
|
|
validMatches.push(m);
|
|
for(var k = start; k < end; k++) {
|
|
occupied[k] = 1;
|
|
}
|
|
}
|
|
|
|
if(validMatches.length === 0) {
|
|
return [{type: "plain-text", text: text}];
|
|
}
|
|
|
|
validMatches.sort(function(a,b){ return a.index - b.index; });
|
|
|
|
var newParseTree = [];
|
|
var curPos = 0;
|
|
|
|
for(var x = 0; x < validMatches.length; x++) {
|
|
var mm = validMatches[x];
|
|
var s = mm.index;
|
|
var e = s + mm.length;
|
|
|
|
if(s > curPos) {
|
|
newParseTree.push({ type: "plain-text", text: text.substring(curPos,s) });
|
|
}
|
|
|
|
var toTitle = this.tiddlerTitleInfo.titles[mm.titleIndex];
|
|
var matchedText = text.substring(s,e);
|
|
|
|
newParseTree.push({
|
|
type: "link",
|
|
attributes: {
|
|
to: {type: "string", value: toTitle},
|
|
"class": {type: "string", value: "tc-freelink"}
|
|
},
|
|
children: [{
|
|
type: "plain-text",
|
|
text: matchedText
|
|
}]
|
|
});
|
|
|
|
curPos = e;
|
|
}
|
|
|
|
if(curPos < text.length) {
|
|
newParseTree.push({ type: "plain-text", text: text.substring(curPos) });
|
|
}
|
|
|
|
return newParseTree;
|
|
};
|
|
|
|
function computeTiddlerTitleInfo(self,ignoreCase) {
|
|
var targetFilterText = self.wiki.getTiddlerText(TITLE_TARGET_FILTER),
|
|
titles = targetFilterText ?
|
|
self.wiki.filterTiddlers(targetFilterText,$tw.rootWidget) :
|
|
self.wiki.allTitles();
|
|
|
|
if(!titles || titles.length === 0) {
|
|
return { titles: [], ac: new AhoCorasick() };
|
|
}
|
|
|
|
var validTitles = [];
|
|
for(var i = 0; i < titles.length; i++) {
|
|
var t = titles[i];
|
|
if(t && t.length > 0 && t.substring(0,3) !== "$:/") {
|
|
validTitles.push(t);
|
|
}
|
|
}
|
|
|
|
validTitles.sort(function(a,b) {
|
|
var d = b.length - a.length;
|
|
if(d !== 0) return d;
|
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
});
|
|
|
|
var ac = new AhoCorasick();
|
|
for(var j = 0; j < validTitles.length; j++) {
|
|
var title = validTitles[j];
|
|
var pattern = ignoreCase ? title.toLowerCase() : title;
|
|
ac.addPattern(pattern,j);
|
|
}
|
|
|
|
try {
|
|
ac.buildFailureLinks();
|
|
} catch(e) {
|
|
return { titles: [], ac: new AhoCorasick() };
|
|
}
|
|
|
|
return { titles: validTitles, ac: ac };
|
|
}
|
|
|
|
TextNodeWidget.prototype.isWithinButtonOrLink = function() {
|
|
var widget = this.parentWidget;
|
|
while(widget) {
|
|
if(widget instanceof ButtonWidget ||
|
|
widget instanceof LinkWidget ||
|
|
((widget instanceof ElementWidget) && widget.parseTreeNode.tag === "a")) {
|
|
return true;
|
|
}
|
|
widget = widget.parentWidget;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
TextNodeWidget.prototype.refresh = function(changedTiddlers) {
|
|
var self = this;
|
|
var changedAttributes = this.computeAttributes();
|
|
var titlesHaveChanged = false;
|
|
|
|
if(changedTiddlers) {
|
|
$tw.utils.each(changedTiddlers,function(change,title) {
|
|
if(titlesHaveChanged) return;
|
|
|
|
if(title === WORD_BOUNDARY_TIDDLER || title === TITLE_TARGET_FILTER) {
|
|
titlesHaveChanged = true;
|
|
return;
|
|
}
|
|
|
|
if(title.substring(0,3) === "$:/") {
|
|
return;
|
|
}
|
|
|
|
if(change && change.isDeleted) {
|
|
if(self.tiddlerTitleInfo && self.tiddlerTitleInfo.titles && self.tiddlerTitleInfo.titles.indexOf(title) !== -1) {
|
|
titlesHaveChanged = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
var tiddler = self.wiki.getTiddler(title);
|
|
if(tiddler && tiddler.hasField("draft.of")) {
|
|
return;
|
|
}
|
|
|
|
if(!self.tiddlerTitleInfo || !self.tiddlerTitleInfo.titles || self.tiddlerTitleInfo.titles.indexOf(title) === -1) {
|
|
titlesHaveChanged = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
var wordBoundaryChanged = !!(changedTiddlers && changedTiddlers[WORD_BOUNDARY_TIDDLER]);
|
|
|
|
if(changedAttributes.text || titlesHaveChanged || wordBoundaryChanged) {
|
|
if(titlesHaveChanged) {
|
|
self.wiki.clearCache("tiddler-title-info-insensitive");
|
|
self.wiki.clearCache("tiddler-title-info-sensitive");
|
|
}
|
|
this.refreshSelf();
|
|
return true;
|
|
}
|
|
|
|
if(changedTiddlers) {
|
|
return this.refreshChildren(changedTiddlers);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
exports.text = TextNodeWidget;
|