From ca1a4a31706d0c03673fe4c414c645ed826b21c6 Mon Sep 17 00:00:00 2001 From: shu Date: Wed, 19 Aug 2026 15:56:06 +0800 Subject: 精简 Wiki 构建与运行方式 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将浏览器端工具整理为 utilities 插件,移除 Docker、Org GTD 和 files API,并补充单文件构建及 Node.js 运行入口。 --- plugins/utilities/copy-codeblock-widget.tid | 72 ++++ plugins/utilities/current-tiddler-toc-sidebar.tid | 69 ++++ plugins/utilities/current-tiddler-toc-widget.tid | 284 +++++++++++++++ plugins/utilities/highlight-caddyfile.tid | 65 ++++ plugins/utilities/highlight-json5.tid | 59 +++ plugins/utilities/highlight-tiddlywiki.tid | 191 ++++++++++ plugins/utilities/journal-calendar-widget.tid | 415 ++++++++++++++++++++++ plugins/utilities/plugin.info | 11 + plugins/utilities/readme.tid | 8 + 9 files changed, 1174 insertions(+) create mode 100644 plugins/utilities/copy-codeblock-widget.tid create mode 100644 plugins/utilities/current-tiddler-toc-sidebar.tid create mode 100644 plugins/utilities/current-tiddler-toc-widget.tid create mode 100644 plugins/utilities/highlight-caddyfile.tid create mode 100644 plugins/utilities/highlight-json5.tid create mode 100644 plugins/utilities/highlight-tiddlywiki.tid create mode 100644 plugins/utilities/journal-calendar-widget.tid create mode 100644 plugins/utilities/plugin.info create mode 100644 plugins/utilities/readme.tid (limited to 'plugins') diff --git a/plugins/utilities/copy-codeblock-widget.tid b/plugins/utilities/copy-codeblock-widget.tid new file mode 100644 index 0000000..2251c62 --- /dev/null +++ b/plugins/utilities/copy-codeblock-widget.tid @@ -0,0 +1,72 @@ +title: $:/plugins/local/copy-codeblock/copy-codeblock.js +type: application/javascript +module-type: widget + +/*\ +title: $:/plugins/local/copy-codeblock/copy-codeblock.js +type: application/javascript +module-type: widget + +Adds a copy button to code blocks. +\*/ +(function() { +"use strict"; + +var CodeBlockWidget = require("$:/core/modules/widgets/codeblock.js").codeblock; + +var CopyCodeBlockWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +CopyCodeBlockWidget.prototype = new CodeBlockWidget(); +CopyCodeBlockWidget.prototype.constructor = CopyCodeBlockWidget; + +CopyCodeBlockWidget.prototype.postRender = function() { + var self = this, + preNode = this.domNodes[0], + previousNode, + button; + + // Preserve syntax highlighting supplied by the highlight plugin. + if(CodeBlockWidget.prototype.postRender) { + CodeBlockWidget.prototype.postRender.call(this); + } + + if(!$tw.browser || !preNode || preNode.isTiddlyWikiFakeDom) { + return; + } + + // The system code view already renders its own copy button. + previousNode = preNode.previousElementSibling; + if(previousNode && previousNode.querySelector && previousNode.querySelector(".tc-image-copy-clipboard")) { + return; + } + + preNode.style.position = "relative"; + preNode.style.paddingRight = "5em"; + + button = this.document.createElement("button"); + button.setAttribute("type","button"); + button.setAttribute("aria-label","Copy code"); + button.setAttribute("title","Copy code"); + button.className = "tc-btn-invisible tc-copy-codeblock-button"; + button.textContent = "Copy"; + button.style.position = "absolute"; + button.style.top = "0.5em"; + button.style.right = "0.5em"; + button.style.zIndex = "1"; + button.style.padding = "0.2em 0.45em"; + button.style.cursor = "pointer"; + + button.addEventListener("click",function(event) { + event.preventDefault(); + event.stopPropagation(); + $tw.utils.copyToClipboard(self.getAttribute("code","")); + }); + + preNode.appendChild(button); +}; + +exports.codeblock = CopyCodeBlockWidget; + +})(); diff --git a/plugins/utilities/current-tiddler-toc-sidebar.tid b/plugins/utilities/current-tiddler-toc-sidebar.tid new file mode 100644 index 0000000..9d73d6d --- /dev/null +++ b/plugins/utilities/current-tiddler-toc-sidebar.tid @@ -0,0 +1,69 @@ +title: $:/plugins/local/current-tiddler-toc/sidebar +type: text/vnd.tiddlywiki +tags: $:/tags/SideBar +caption: 目录 +list-after: $:/core/ui/SideBar/More + + + +<$current-tiddler-toc/> diff --git a/plugins/utilities/current-tiddler-toc-widget.tid b/plugins/utilities/current-tiddler-toc-widget.tid new file mode 100644 index 0000000..a68e449 --- /dev/null +++ b/plugins/utilities/current-tiddler-toc-widget.tid @@ -0,0 +1,284 @@ +title: $:/plugins/local/current-tiddler-toc/widget.js +type: application/javascript +module-type: widget + +/*\ +title: $:/plugins/local/current-tiddler-toc/widget.js +type: application/javascript +module-type: widget + +Displays a table of contents for the current story tiddler. +\*/ +(function() { +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget; + +var CurrentTiddlerTocWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +CurrentTiddlerTocWidget.prototype = new Widget(); +CurrentTiddlerTocWidget.prototype.constructor = CurrentTiddlerTocWidget; + +CurrentTiddlerTocWidget.prototype.render = function(parent,nextSibling) { + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + + var container = this.document.createElement("div"); + container.className = "tc-current-tiddler-toc"; + + parent.insertBefore(container,nextSibling); + this.domNodes.push(container); + this.containerNode = container; + + if($tw.browser) { + var self = this, + windowObject = this.document.defaultView || window; + + this.windowObject = windowObject; + this.resizeHandler = function() { + self.updateMaxHeight(); + }; + windowObject.addEventListener("resize",this.resizeHandler); + if(windowObject.visualViewport) { + windowObject.visualViewport.addEventListener("resize",this.resizeHandler); + } + } + + this.scheduleBuild(); +}; + +CurrentTiddlerTocWidget.prototype.execute = function() { +}; + +CurrentTiddlerTocWidget.prototype.getStoryTitles = function() { + return this.wiki.getTiddlerList("$:/StoryList") || []; +}; + +CurrentTiddlerTocWidget.prototype.findTiddlerFrame = function(title) { + var frames = this.document.querySelectorAll(".tc-tiddler-frame[data-tiddler-title]"), + index; + + for(index = 0; index < frames.length; index++) { + var frame = frames[index], + frameTitle = frame.getAttribute("data-tiddler-title"), + frameTiddler; + + if(frameTitle === title) { + return frame; + } + + frameTiddler = this.wiki.getTiddler(frameTitle); + + if(frameTiddler && (frameTiddler.fields["draft.of"] === title || frameTiddler.fields["draft.title"] === title)) { + return frame; + } + } + + return null; +}; + +CurrentTiddlerTocWidget.prototype.clearContainer = function() { + while(this.containerNode && this.containerNode.firstChild) { + this.containerNode.removeChild(this.containerNode.firstChild); + } +}; + +CurrentTiddlerTocWidget.prototype.showMessage = function(message) { + var messageNode = this.document.createElement("div"); + + messageNode.className = "tc-current-tiddler-toc-empty"; + messageNode.textContent = message; + this.containerNode.appendChild(messageNode); +}; + +CurrentTiddlerTocWidget.prototype.updateMaxHeight = function() { + if(!this.containerNode || !this.containerNode.parentNode || !this.windowObject) { + return; + } + + var viewport = this.windowObject.visualViewport, + viewportHeight = viewport ? viewport.height : this.windowObject.innerHeight, + top = this.containerNode.getBoundingClientRect().top, + availableHeight = Math.max(160,Math.floor(viewportHeight - top - 16)); + + this.containerNode.style.maxHeight = availableHeight + "px"; +}; + +CurrentTiddlerTocWidget.prototype.getHeadings = function(frame) { + var body = frame.querySelector(".tc-tiddler-body"); + + if(!body) { + body = frame.querySelector(".tc-tiddler-preview-preview"); + } + + if(!body) { + return []; + } + + var headingNodes = body.querySelectorAll("h1, h2, h3, h4, h5, h6"), + headings = [], + index; + + for(index = 0; index < headingNodes.length; index++) { + var heading = headingNodes[index]; + + // Ignore headings that belong to nested, independent tiddler frames. + if(heading.closest && heading.closest(".tc-tiddler-frame") !== frame) { + continue; + } + + headings.push({ + node: heading, + level: parseInt(heading.tagName.substring(1),10), + text: heading.textContent.trim() || "未命名标题" + }); + } + + return headings; +}; + +CurrentTiddlerTocWidget.prototype.addTiddlerGroup = function(title,frame) { + var self = this, + groupNode = this.document.createElement("section"), + titleButton = this.document.createElement("button"); + + groupNode.className = "tc-current-tiddler-toc-group"; + titleButton.setAttribute("type","button"); + titleButton.className = "tc-btn-invisible tc-current-tiddler-toc-title"; + titleButton.textContent = title; + titleButton.title = "滚动到条目:" + title; + titleButton.addEventListener("click",function(event) { + event.preventDefault(); + frame.scrollIntoView({ + behavior: "smooth", + block: "start" + }); + }); + groupNode.appendChild(titleButton); + + var headings = this.getHeadings(frame); + + if(!headings.length) { + var emptyNode = this.document.createElement("div"); + emptyNode.className = "tc-current-tiddler-toc-group-empty"; + emptyNode.textContent = "没有标题"; + groupNode.appendChild(emptyNode); + this.containerNode.appendChild(groupNode); + return; + } + + var minimumLevel = headings.reduce(function(result,heading) { + return Math.min(result,heading.level); + },6), + listNode = this.document.createElement("ol"); + + listNode.className = "tc-current-tiddler-toc-list"; + groupNode.appendChild(listNode); + + headings.forEach(function(heading) { + var itemNode = self.document.createElement("li"), + buttonNode = self.document.createElement("button"); + + itemNode.className = "tc-current-tiddler-toc-item"; + itemNode.style.paddingLeft = ((heading.level - minimumLevel) * 1.1) + "em"; + + buttonNode.setAttribute("type","button"); + buttonNode.className = "tc-btn-invisible tc-current-tiddler-toc-link"; + buttonNode.textContent = heading.text; + buttonNode.title = heading.text; + buttonNode.addEventListener("click",function(event) { + event.preventDefault(); + heading.node.scrollIntoView({ + behavior: "smooth", + block: "start" + }); + }); + + itemNode.appendChild(buttonNode); + listNode.appendChild(itemNode); + }); + + this.containerNode.appendChild(groupNode); +}; + +CurrentTiddlerTocWidget.prototype.build = function() { + if(!this.containerNode || !this.containerNode.parentNode) { + return; + } + + this.clearContainer(); + + var titles = this.getStoryTitles(), + index, + visibleCount = 0; + + if(!titles.length) { + this.showMessage("当前没有打开的条目"); + this.updateMaxHeight(); + return; + } + + for(index = 0; index < titles.length; index++) { + var title = titles[index], + frame = this.findTiddlerFrame(title); + + if(frame) { + this.addTiddlerGroup(title,frame); + visibleCount += 1; + } + } + + if(!visibleCount) { + this.showMessage("打开的条目尚未显示"); + } + + this.updateMaxHeight(); +}; + +CurrentTiddlerTocWidget.prototype.scheduleBuild = function() { + var self = this; + + if(!$tw.browser) { + return; + } + + if(this.buildTimer) { + clearTimeout(this.buildTimer); + } + + // Wait until the story river has completed the current DOM update. + this.buildTimer = setTimeout(function() { + self.buildTimer = null; + self.build(); + },0); +}; + +CurrentTiddlerTocWidget.prototype.refresh = function() { + this.computeAttributes(); + this.scheduleBuild(); + return false; +}; + +CurrentTiddlerTocWidget.prototype.removeChildDomNodes = function() { + if(this.buildTimer) { + clearTimeout(this.buildTimer); + this.buildTimer = null; + } + if(this.windowObject && this.resizeHandler) { + this.windowObject.removeEventListener("resize",this.resizeHandler); + if(this.windowObject.visualViewport) { + this.windowObject.visualViewport.removeEventListener("resize",this.resizeHandler); + } + this.resizeHandler = null; + this.windowObject = null; + } + + Widget.prototype.removeChildDomNodes.call(this); +}; + +exports["current-tiddler-toc"] = CurrentTiddlerTocWidget; + +})(); diff --git a/plugins/utilities/highlight-caddyfile.tid b/plugins/utilities/highlight-caddyfile.tid new file mode 100644 index 0000000..8427c3f --- /dev/null +++ b/plugins/utilities/highlight-caddyfile.tid @@ -0,0 +1,65 @@ +tiddlers/highlight-caddyfile.tid +title: $:/plugins/local/highlight-languages/caddyfile.js +type: application/javascript +module-type: highlight + +/*\ +title: $:/plugins/local/highlight-languages/caddyfile.js +type: application/javascript +module-type: highlight + +Caddyfile language definition for highlight.js. +\*/ +"use strict"; + +hljs.registerLanguage("caddyfile",function(hljs) { + return { + name: "Caddyfile", + aliases: ["caddy"], + case_insensitive: true, + contains: [ + { + className: "comment", + begin: /#/, + end: /$/ + }, + { + className: "section", + begin: /^\s*(?=[^#\n{]*(?:[.:,]|\blocalhost\b))(?:https?:\/\/)?[^\s#{}]+(?:\s*,\s*[^\s#{}]+)*(?=\s*\{?\s*$)/, + relevance: 10 + }, + { + className: "keyword", + begin: /^\s*[A-Za-z_][A-Za-z0-9_-]*(?=\s|$)/, + relevance: 1 + }, + { + className: "meta", + match: /@[A-Za-z0-9_-]+/ + }, + { + className: "variable", + match: /\{\$?[A-Za-z_][A-Za-z0-9_.\-[\]*]*\}/ + }, + { + className: "title", + match: /\([A-Za-z_][A-Za-z0-9_-]*\)/ + }, + hljs.QUOTE_STRING_MODE, + hljs.APOS_STRING_MODE, + { + className: "literal", + match: /\b(?:true|false|on|off)\b/ + }, + { + className: "number", + match: /\b\d+(?:\.\d+)?(?:ns|us|ms|s|m|h|d|kb|mb|gb|tb)?\b/i, + relevance: 0 + }, + { + className: "punctuation", + match: /[{}]/ + } + ] + }; +}); diff --git a/plugins/utilities/highlight-json5.tid b/plugins/utilities/highlight-json5.tid new file mode 100644 index 0000000..fae9bd4 --- /dev/null +++ b/plugins/utilities/highlight-json5.tid @@ -0,0 +1,59 @@ +title: $:/plugins/local/highlight-languages/json5.js +type: application/javascript +module-type: highlight + +/*\ +title: $:/plugins/local/highlight-languages/json5.js +type: application/javascript +module-type: highlight + +JSON5 language definition for highlight.js. +\*/ +"use strict"; + +hljs.registerLanguage("json5",function(hljs) { + return { + name: "JSON5", + aliases: ["json5"], + keywords: { + literal: "true false null" + }, + contains: [ + hljs.C_LINE_COMMENT_MODE, + hljs.C_BLOCK_COMMENT_MODE, + { + className: "attr", + match: /[A-Za-z_$][0-9A-Za-z_$]*(?=\s*:)/ + }, + { + className: "attr", + match: /"(?:\\.|[^"\\\r\n])*"(?=\s*:)/ + }, + { + className: "attr", + match: /'(?:\\.|[^'\\\r\n])*'(?=\s*:)/ + }, + hljs.QUOTE_STRING_MODE, + hljs.APOS_STRING_MODE, + { + className: "number", + variants: [ + { + begin: /[+-]?(?:Infinity|NaN)\b/ + }, + { + begin: /[+-]?0[xX][0-9a-fA-F]+\b/ + }, + { + begin: /[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/ + } + ], + relevance: 0 + }, + { + className: "punctuation", + match: /[{}\[\],:]/ + } + ] + }; +}); diff --git a/plugins/utilities/highlight-tiddlywiki.tid b/plugins/utilities/highlight-tiddlywiki.tid new file mode 100644 index 0000000..dd1a891 --- /dev/null +++ b/plugins/utilities/highlight-tiddlywiki.tid @@ -0,0 +1,191 @@ +title: $:/plugins/local/highlight-languages/tiddlywiki.js +type: application/javascript +module-type: highlight + +/*\ +title: $:/plugins/local/highlight-languages/tiddlywiki.js +type: application/javascript +module-type: highlight + +TiddlyWiki TID and WikiText language definition for highlight.js. +\*/ +"use strict"; + +hljs.registerLanguage("tiddlywiki",function(hljs) { + var systemTiddler = { + className: "symbol", + match: /\$:\/[^\s\][\]<>{}"']+/ + }, + variable = { + className: "variable", + variants: [ + { + begin: /<>/ + }, + { + match: /<[^<>\s]+>/ + } + ] + }, + transclusion = { + className: "template-variable", + variants: [ + { + begin: /\{\{\{/, + end: /\}\}\}/ + }, + { + begin: /\{\{/, + end: /\}\}/ + } + ], + contains: [ + systemTiddler, + variable + ] + }, + link = { + className: "link", + variants: [ + { + begin: /\[\[/, + end: /\]\]/ + }, + { + begin: /\[(?:img|ext)\[/, + end: /\]\]/ + } + ], + contains: [ + systemTiddler + ] + }, + filter = { + className: "selector", + begin: /\[(?=[^\]\n]*[A-Za-z][A-Za-z0-9_-]*\[)/, + end: /\]/, + contains: [ + { + className: "keyword", + match: /[A-Za-z][A-Za-z0-9_.-]*(?=\[)/ + }, + { + className: "string", + begin: /\[/, + end: /\]/, + contains: [ + systemTiddler, + variable + ] + }, + variable, + systemTiddler + ] + }; + + return { + name: "TiddlyWiki", + aliases: [ + "tid", + "wikitext" + ], + contains: [ + { + className: "comment", + begin: // + }, + { + className: "comment", + begin: /\/%/, + end: /%\// + }, + { + className: "meta", + begin: /^\\(?:define|end|function|import|parameters|procedure|rules|whitespace|widget)\b/, + end: /$/, + contains: [ + variable, + systemTiddler, + hljs.QUOTE_STRING_MODE, + hljs.APOS_STRING_MODE + ] + }, + { + className: "attr", + match: /^[A-Za-z0-9_.-]+(?=\s*:)/ + }, + { + className: "section", + begin: /^!{1,6}\s+/, + end: /$/ + }, + { + className: "bullet", + match: /^[*#;:]+\s+/ + }, + { + className: "quote", + begin: /^>{1,3}\s+/, + end: /$/ + }, + { + className: "tag", + begin: /<\/?\$?[A-Za-z][A-Za-z0-9_.:-]*/, + end: /\/?>/, + contains: [ + { + className: "attr", + match: /[A-Za-z_$][A-Za-z0-9_.:-]*(?=\s*=)/ + }, + hljs.QUOTE_STRING_MODE, + hljs.APOS_STRING_MODE, + transclusion, + variable, + filter, + systemTiddler + ] + }, + { + className: "template-tag", + begin: /<>/, + contains: [ + hljs.QUOTE_STRING_MODE, + hljs.APOS_STRING_MODE, + systemTiddler + ] + }, + transclusion, + link, + filter, + systemTiddler, + { + className: "code", + begin: /`+/, + end: /`+/ + }, + { + className: "strong", + begin: /''/, + end: /''/ + }, + { + className: "emphasis", + begin: /\/\//, + end: /\/\// + }, + { + className: "emphasis", + begin: /__/, + end: /__/ + }, + { + className: "deletion", + begin: /~~/, + end: /~~/ + } + ] + }; +}); diff --git a/plugins/utilities/journal-calendar-widget.tid b/plugins/utilities/journal-calendar-widget.tid new file mode 100644 index 0000000..c16276d --- /dev/null +++ b/plugins/utilities/journal-calendar-widget.tid @@ -0,0 +1,415 @@ +title: $:/plugins/local/journal-calendar/widget.js +type: application/javascript +module-type: widget + +/*\ +title: $:/plugins/local/journal-calendar/widget.js +type: application/javascript +module-type: widget + +Renders journal tiddlers as monthly calendars. +\*/ +(function() { +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget, + VIEW_STATE_TITLE = "$:/state/journal-calendar/view", + MONTH_STATE_TITLE = "$:/state/journal-calendar/month"; + +var JournalCalendarWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +JournalCalendarWidget.prototype = new Widget(); +JournalCalendarWidget.prototype.constructor = JournalCalendarWidget; + +function pad(value) { + return value < 10 ? "0" + value : "" + value; +} + +function dateKey(year,month,day) { + return year + "-" + pad(month) + "-" + pad(day); +} + +function monthKey(year,month) { + return year + "-" + pad(month); +} + +function isValidMonth(value) { + var match = /^(\d{4})-(\d{2})$/.exec(value || ""); + return !!match && Number(match[2]) >= 1 && Number(match[2]) <= 12; +} + +function parseMonth(value) { + var parts = value.split("-"); + return { + year: Number(parts[0]), + month: Number(parts[1]) + }; +} + +function formatMonth(value) { + var month = parseMonth(value); + return month.year + "年" + month.month + "月"; +} + +JournalCalendarWidget.prototype.render = function(parent,nextSibling) { + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + + var container = this.document.createElement("div"); + container.className = "tc-journal-calendar"; + parent.insertBefore(container,nextSibling); + this.domNodes.push(container); + this.containerNode = container; + + this.renderCalendar(); +}; + +JournalCalendarWidget.prototype.execute = function() { + this.tag = this.getAttribute("tag","日志"); + this.view = this.getAttribute("view","switchable"); + this.month = this.getAttribute("month",""); +}; + +JournalCalendarWidget.prototype.getJournalDate = function(tiddler) { + var titleMatch = /^(\d{4})年(\d{2})月(\d{2})日$/.exec(tiddler.fields.title), + date; + + if(titleMatch) { + date = new Date(Number(titleMatch[1]),Number(titleMatch[2]) - 1,Number(titleMatch[3])); + if(date.getFullYear() === Number(titleMatch[1]) && date.getMonth() === Number(titleMatch[2]) - 1 && date.getDate() === Number(titleMatch[3])) { + return date; + } + } + return null; +}; + +JournalCalendarWidget.prototype.getJournalData = function() { + var titles = this.wiki.getTiddlersWithTag(this.tag), + byDate = Object.create(null), + months = Object.create(null), + self = this; + + titles.forEach(function(title) { + var tiddler = self.wiki.getTiddler(title); + if(!tiddler || tiddler.fields["draft.of"]) { + return; + } + + var date = self.getJournalDate(tiddler); + if(!date) { + return; + } + + var day = dateKey(date.getFullYear(),date.getMonth() + 1,date.getDate()), + month = day.substring(0,7); + byDate[day] = { + title: title, + tags: (tiddler.fields.tags || []).filter(function(tag) { + return tag !== self.tag; + }) + }; + months[month] = true; + }); + + return { + byDate: byDate, + months: Object.keys(months).sort().reverse() + }; +}; + +JournalCalendarWidget.prototype.getCurrentMonth = function() { + if(isValidMonth(this.month)) { + return this.month; + } + + var stateMonth = this.wiki.getTiddlerText(MONTH_STATE_TITLE,""); + if(isValidMonth(stateMonth)) { + return stateMonth; + } + + var now = new Date(); + return monthKey(now.getFullYear(),now.getMonth() + 1); +}; + +JournalCalendarWidget.prototype.getCurrentView = function() { + if(this.view === "month" || this.view === "all") { + return this.view; + } + return this.wiki.getTiddlerText(VIEW_STATE_TITLE,"month") === "all" ? "all" : "month"; +}; + +JournalCalendarWidget.prototype.setState = function(title,value) { + this.wiki.addTiddler({ + title: title, + text: value + }); +}; + +JournalCalendarWidget.prototype.getMonthJournals = function(value,byDate) { + return Object.keys(byDate).filter(function(date) { + return date.substring(0,7) === value; + }).sort().map(function(date) { + return { + date: date, + title: byDate[date].title, + tags: byDate[date].tags + }; + }); +}; + +JournalCalendarWidget.prototype.createButton = function(label,className,handler) { + var button = this.document.createElement("button"); + button.setAttribute("type","button"); + button.className = className; + button.textContent = label; + button.addEventListener("click",handler); + return button; +}; + +JournalCalendarWidget.prototype.renderOpenMonthButton = function(value,journals) { + if(!journals.length) { + return null; + } + + var self = this, + button = this.createButton("展开本月日志(" + journals.length + ")","tc-btn-invisible tc-journal-calendar-expand",function(event) { + self.openMonthJournals(journals,event.currentTarget,event); + }); + button.title = "打开" + formatMonth(value) + "的全部日志"; + button.setAttribute("aria-label","打开" + formatMonth(value) + "的全部日志,共" + journals.length + "篇"); + return button; +}; + +JournalCalendarWidget.prototype.renderViewSwitcher = function(currentView) { + if(this.view !== "switchable") { + return null; + } + + var self = this, + controls = this.document.createElement("div"); + controls.className = "tc-journal-calendar-view-switcher"; + + controls.appendChild(this.createButton("单月视图","tc-btn-invisible tc-journal-calendar-switch" + (currentView === "month" ? " tc-selected" : ""),function() { + self.setState(VIEW_STATE_TITLE,"month"); + })); + controls.appendChild(this.createButton("全部月份","tc-btn-invisible tc-journal-calendar-switch" + (currentView === "all" ? " tc-selected" : ""),function() { + self.setState(VIEW_STATE_TITLE,"all"); + })); + return controls; +}; + +JournalCalendarWidget.prototype.renderMonthControls = function(selectedMonth) { + if(isValidMonth(this.month)) { + return null; + } + + var self = this, + selected = parseMonth(selectedMonth), + controls = this.document.createElement("div"); + controls.className = "tc-journal-calendar-month-controls"; + + function setOffset(offset) { + var date = new Date(selected.year,selected.month - 1 + offset,1); + self.setState(MONTH_STATE_TITLE,monthKey(date.getFullYear(),date.getMonth() + 1)); + } + + controls.appendChild(this.createButton("上个月","tc-btn-invisible tc-journal-calendar-nav",function() { + setOffset(-1); + })); + controls.appendChild(this.createButton("本月","tc-btn-invisible tc-journal-calendar-nav",function() { + var now = new Date(); + self.setState(MONTH_STATE_TITLE,monthKey(now.getFullYear(),now.getMonth() + 1)); + })); + controls.appendChild(this.createButton("下个月","tc-btn-invisible tc-journal-calendar-nav",function() { + setOffset(1); + })); + return controls; +}; + +JournalCalendarWidget.prototype.renderToolbar = function(currentView,selectedMonth,byDate) { + var toolbar = this.document.createElement("div"), + monthSummary = this.document.createElement("div"), + monthTitle = this.document.createElement("h2"), + monthControls, + expandButton, + viewSwitcher; + + toolbar.className = "tc-journal-calendar-toolbar"; + monthSummary.className = "tc-journal-calendar-month-summary"; + monthTitle.className = "tc-journal-calendar-current-month"; + monthTitle.textContent = currentView === "all" ? "全部月份" : formatMonth(selectedMonth); + monthSummary.appendChild(monthTitle); + + if(currentView === "month") { + monthControls = this.renderMonthControls(selectedMonth); + if(monthControls) { + monthSummary.appendChild(monthControls); + } + expandButton = this.renderOpenMonthButton(selectedMonth,this.getMonthJournals(selectedMonth,byDate)); + if(expandButton) { + monthSummary.appendChild(expandButton); + } + } + toolbar.appendChild(monthSummary); + + viewSwitcher = this.renderViewSwitcher(currentView); + if(viewSwitcher) { + toolbar.appendChild(viewSwitcher); + } + this.containerNode.appendChild(toolbar); +}; + +JournalCalendarWidget.prototype.navigateTo = function(title,node,event) { + var bounds = node.getBoundingClientRect(); + this.dispatchEvent({ + type: "tm-navigate", + navigateTo: title, + navigateFromTitle: this.getVariable("storyTiddler"), + navigateFromNode: this, + navigateFromClientRect: { + top: bounds.top, + left: bounds.left, + width: bounds.width, + right: bounds.right, + bottom: bounds.bottom, + height: bounds.height + }, + navigateSuppressNavigation: event.metaKey || event.ctrlKey || event.button === 1 + }); +}; + +JournalCalendarWidget.prototype.openMonthJournals = function(journals,node,event) { + var self = this; + journals.forEach(function(journal) { + self.navigateTo(journal.title,node,event); + }); +}; + +JournalCalendarWidget.prototype.renderMonth = function(value,byDate,showHeading) { + var self = this, + selected = parseMonth(value), + section = this.document.createElement("section"), + monthHeader = this.document.createElement("div"), + heading, + expandButton, + viewport = this.document.createElement("div"), + grid = this.document.createElement("div"), + journals = this.getMonthJournals(value,byDate), + weekdays = ["一","二","三","四","五","六","日"], + firstWeekday = (new Date(selected.year,selected.month - 1,1).getDay() + 6) % 7, + daysInMonth = new Date(selected.year,selected.month,0).getDate(), + today = new Date(), + index; + + section.className = "tc-journal-calendar-month"; + monthHeader.className = "tc-journal-calendar-month-header"; + if(showHeading) { + heading = this.document.createElement("h2"); + heading.className = "tc-journal-calendar-month-title"; + heading.textContent = formatMonth(value); + monthHeader.appendChild(heading); + expandButton = this.renderOpenMonthButton(value,journals); + if(expandButton) { + monthHeader.appendChild(expandButton); + } + section.appendChild(monthHeader); + } + viewport.className = "tc-journal-calendar-viewport"; + grid.className = "tc-journal-calendar-grid"; + + weekdays.forEach(function(weekday) { + var weekdayNode = self.document.createElement("div"); + weekdayNode.className = "tc-journal-calendar-weekday"; + weekdayNode.textContent = weekday; + grid.appendChild(weekdayNode); + }); + + for(index = 0; index < firstWeekday; index++) { + var blank = this.document.createElement("div"); + blank.className = "tc-journal-calendar-day tc-journal-calendar-day-empty"; + grid.appendChild(blank); + } + + for(var day = 1; day <= daysInMonth; day++) { + var key = dateKey(selected.year,selected.month,day), + journal = byDate[key], + dayNode = this.document.createElement("div"), + numberNode; + + dayNode.className = "tc-journal-calendar-day" + (journal ? " tc-journal-calendar-day-has-journal" : ""); + if(selected.year === today.getFullYear() && selected.month === today.getMonth() + 1 && day === today.getDate()) { + dayNode.className += " tc-journal-calendar-day-today"; + } + + if(journal) { + numberNode = this.createButton(day,"tc-btn-invisible tc-journal-calendar-day-number tc-journal-calendar-day-link",function(title) { + return function(event) { + self.navigateTo(title,event.currentTarget,event); + }; + }(journal.title)); + numberNode.title = "打开日志:" + journal.title; + numberNode.setAttribute("aria-label","打开日志:" + journal.title); + } else { + numberNode = this.document.createElement("div"); + numberNode.className = "tc-journal-calendar-day-number"; + numberNode.textContent = day; + } + dayNode.appendChild(numberNode); + + if(journal) { + journal.tags.forEach(function(tag) { + var tagButton = self.createButton(tag,"tc-btn-invisible tc-tag-label tc-journal-calendar-tag",function(event) { + self.navigateTo(tag,event.currentTarget,event); + }); + tagButton.title = "打开标签:" + tag; + tagButton.setAttribute("aria-label","打开标签:" + tag); + dayNode.appendChild(tagButton); + }); + } + grid.appendChild(dayNode); + } + + viewport.appendChild(grid); + section.appendChild(viewport); + this.containerNode.appendChild(section); +}; + +JournalCalendarWidget.prototype.renderCalendar = function() { + var data = this.getJournalData(), + currentView = this.getCurrentView(), + selectedMonth = this.getCurrentMonth(); + + this.renderToolbar(currentView,selectedMonth,data.byDate); + + if(currentView === "all") { + if(!data.months.length) { + var empty = this.document.createElement("div"); + empty.className = "tc-journal-calendar-empty"; + empty.textContent = "目前没有日志。"; + this.containerNode.appendChild(empty); + return; + } + data.months.forEach(function(month) { + this.renderMonth(month,data.byDate,true); + },this); + return; + } + + this.renderMonth(selectedMonth,data.byDate,false); +}; + +JournalCalendarWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if(Object.keys(changedAttributes).length || Object.keys(changedTiddlers).length) { + this.refreshSelf(); + return true; + } + return false; +}; + +exports["journal-calendar"] = JournalCalendarWidget; + +})(); diff --git a/plugins/utilities/plugin.info b/plugins/utilities/plugin.info new file mode 100644 index 0000000..0a2c117 --- /dev/null +++ b/plugins/utilities/plugin.info @@ -0,0 +1,11 @@ +{ + "title": "$:/plugins/local/utilities", + "name": "Local Utilities", + "description": "Small browser-side utilities used by this wiki", + "author": "local", + "version": "1.1.1", + "core-version": ">=5.4.1", + "plugin-type": "plugin", + "dependents": ["$:/plugins/tiddlywiki/highlight"], + "list": "readme" +} diff --git a/plugins/utilities/readme.tid b/plugins/utilities/readme.tid new file mode 100644 index 0000000..e2b35a9 --- /dev/null +++ b/plugins/utilities/readme.tid @@ -0,0 +1,8 @@ +title: $:/plugins/local/utilities/readme +type: text/vnd.tiddlywiki + +! Local Utilities + +收纳当前条目目录、代码块复制、日志日历,以及 Caddyfile、JSON5 和 TiddlyWiki 的 Highlight 语言扩展。 + +Highlight 语言扩展依赖官方插件:`$:/plugins/tiddlywiki/highlight`。 -- cgit v1.2.3