From 487e96b588066e85e98b8608f6bb262a15ba156c Mon Sep 17 00:00:00 2001 From: luxick Date: Fri, 30 Jan 2026 11:12:50 +0100 Subject: [PATCH] Add date fixer functions --- README.md | 16 +++ action.php | 18 +++ deploy.sh | 28 +++-- images/date-fix-all.svg | 10 ++ images/date-fix.svg | 8 ++ js/date-fix.js | 235 ++++++++++++++++++++++++++++++++++++++++ lang/en/lang.php | 2 + 7 files changed, 305 insertions(+), 12 deletions(-) create mode 100644 images/date-fix-all.svg create mode 100644 images/date-fix.svg create mode 100644 js/date-fix.js diff --git a/README.md b/README.md index c20b80c..6845193 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,22 @@ When editing a page, click the code block button (angle brackets icon `<>`) in t This complements DokuWiki's built-in monospace formatting (`''`) by providing quick access to HTML code blocks. +### 0.1) Editor toolbar: Date Fix + +The plugin adds two toolbar buttons for normalizing timestamps while editing: + +- **Date Fix**: Converts the selected timestamp to `YYYY-MM-DD` (or `YYYY-MM-DD HH:MM:SS` if time is included). +- **Date Fix (All)**: Scans the whole page and normalizes any recognizable timestamps. + +Supported input examples include: + +- `2026-01-30` +- `30.01.2026` +- `30 Jan 2026` +- `Jan 30, 2026` +- `2026-01-30 13:45` +- `2026-01-30T13:45:00` + ### 1) List files by glob pattern The `{{directory>...}}` syntax (or `{{files>...}}` for backwards compatibility) can handle both directory listings and glob patterns. When a glob pattern is used, it renders as a table: diff --git a/action.php b/action.php index 6a80d2e..804ca51 100644 --- a/action.php +++ b/action.php @@ -48,6 +48,7 @@ class action_plugin_luxtools extends ActionPlugin "gallery-thumbnails.js", "open-service.js", "scratchpads.js", + "date-fix.js", "linkfavicon.js", "main.js", ]; @@ -100,5 +101,22 @@ class action_plugin_luxtools extends ActionPlugin "close" => "", "block" => false, ]; + + // Date Fix: normalize selected timestamp + $event->data[] = [ + "type" => "LuxtoolsDatefix", + "title" => $this->getLang("toolbar_datefix_title"), + "icon" => "../../plugins/luxtools/images/date-fix.svg", + "key" => "t", + "block" => false, + ]; + + // Date Fix All: normalize all timestamps on page + $event->data[] = [ + "type" => "LuxtoolsDatefixAll", + "title" => $this->getLang("toolbar_datefix_all_title"), + "icon" => "../../plugins/luxtools/images/date-fix-all.svg", + "block" => false, + ]; } } diff --git a/deploy.sh b/deploy.sh index e180d3f..bac9d6f 100755 --- a/deploy.sh +++ b/deploy.sh @@ -9,12 +9,10 @@ set -euo pipefail # ./deploy.sh --dry-run # show what would change # ./deploy.sh /path/to/luxtools # ./deploy.sh --no-delete # don't delete extraneous files at target -# ./deploy.sh --preserve-times # keep source mtimes at target TARGET="/thebe/Web/lib/plugins/luxtools" DRY_RUN=0 DELETE=1 -PRESERVE_TIMES=0 while (($#)); do case "$1" in @@ -26,10 +24,6 @@ while (($#)); do DELETE=0 shift ;; - --preserve-times) - PRESERVE_TIMES=1 - shift - ;; -h|--help) sed -n '1,80p' "$0" exit 0 @@ -85,12 +79,6 @@ RSYNC_ARGS=( --exclude=.DS_Store ) -# DokuWiki's combined CSS (lib/exe/css.php) is cached and invalidated based on source file mtimes. -# When deploying to a mounted/remote filesystem with a different clock, preserving mtimes can make -# DokuWiki think the cache is always newer than your plugin CSS. Avoid that by default. -if (( ! PRESERVE_TIMES )); then - RSYNC_ARGS+=(--no-times --omit-dir-times) -fi if ((DRY_RUN)); then RSYNC_ARGS+=(--dry-run) @@ -105,4 +93,20 @@ echo "Deploying luxtools to: $TARGET/" rsync "${RSYNC_ARGS[@]}" "$SRC_DIR/" "$TARGET/" +# Invalidate DokuWiki cache by touching conf/local.php +# This forces DokuWiki to rebuild JavaScript/CSS bundles +CONF_LOCAL="$(dirname "$TARGET")/../../conf/local.php" +if [[ -f "$CONF_LOCAL" ]]; then + if ((DRY_RUN)); then + echo "(dry-run) Would touch $CONF_LOCAL to invalidate cache" + elif touch "$CONF_LOCAL" 2>/dev/null; then + echo "Cache invalidated (touched conf/local.php)" + else + echo "Note: Cannot touch conf/local.php (permission denied)." + echo " Run 'touch conf/local.php' on the server to clear cache." + fi +else + echo "Note: conf/local.php not found at expected path, skip cache invalidation." +fi + echo "Done." diff --git a/images/date-fix-all.svg b/images/date-fix-all.svg new file mode 100644 index 0000000..f99468b --- /dev/null +++ b/images/date-fix-all.svg @@ -0,0 +1,10 @@ + diff --git a/images/date-fix.svg b/images/date-fix.svg new file mode 100644 index 0000000..41148b1 --- /dev/null +++ b/images/date-fix.svg @@ -0,0 +1,8 @@ + diff --git a/js/date-fix.js b/js/date-fix.js new file mode 100644 index 0000000..5cf30a1 --- /dev/null +++ b/js/date-fix.js @@ -0,0 +1,235 @@ +/* global window, document, DWgetSelection, DWsetSelection, pasteText */ + +(function () { + 'use strict'; + + var Luxtools = window.Luxtools || (window.Luxtools = {}); + var DateFix = Luxtools.DateFix || (Luxtools.DateFix = {}); + + // Month name patterns for regex (English and German) + var MONTH_PATTERN = '(?:jan|feb|m[aä]r|apr|ma[iy]|jun|jul|aug|sep|sept|okt|oct|nov|de[cz])[a-z]*\\.?'; + + // Regex to find date candidates in text for "fix all" feature + var CANDIDATE_REGEX = new RegExp( + '\\b(?:' + + '\\d{4}[-\\/.][\\d]{1,2}[-\\/.][\\d]{1,2}|' + // YYYY-MM-DD + '\\d{1,2}[-\\/.][\\d]{1,2}[-\\/.][\\d]{4}|' + // DD-MM-YYYY + MONTH_PATTERN + '\\s+\\d{1,2}(?:st|nd|rd|th)?[,]?\\s+\\d{4}|' + // Month DD, YYYY + '\\d{1,2}(?:st|nd|rd|th)?\\.?\\s+' + MONTH_PATTERN + '\\s+\\d{4}|' + // DD Month YYYY + '\\d{4}\\s+' + MONTH_PATTERN + '\\s+\\d{1,2}' + // YYYY Month DD + ')(?:[T\\s]+\\d{1,2}:\\d{2}(?::\\d{2})?(?:\\s*(?:am|pm))?)?\\b', + 'gi' + ); + + // Map month names (English and German) to month numbers (1-12) + var MONTH_MAP = { + 'jan': 1, 'januar': 1, 'january': 1, + 'feb': 2, 'februar': 2, 'february': 2, + 'mar': 3, 'mär': 3, 'märz': 3, 'march': 3, 'maerz': 3, + 'apr': 4, 'april': 4, + 'may': 5, 'mai': 5, + 'jun': 6, 'juni': 6, 'june': 6, + 'jul': 7, 'juli': 7, 'july': 7, + 'aug': 8, 'august': 8, + 'sep': 9, 'sept': 9, 'september': 9, + 'oct': 10, 'okt': 10, 'oktober': 10, 'october': 10, + 'nov': 11, 'november': 11, + 'dec': 12, 'dez': 12, 'dezember': 12, 'december': 12 + }; + + function pad2(n) { + return n < 10 ? '0' + n : String(n); + } + + /** + * Look up a month name (English or German) and return its number (1-12). + * Returns null if not found. + */ + function parseMonthName(name) { + if (!name) return null; + var key = name.toLowerCase().replace(/\.$/, ''); + return MONTH_MAP[key] || null; + } + + /** + * Preprocess input to make it parseable by Date. + * Handles formats Date.parse() doesn't understand natively. + */ + function preprocess(input) { + var s = input + .replace(/^\s*[([{"'`]+|[)\]}",'`]+\s*$/g, '') // strip surrounding brackets/quotes + .replace(/\s*(Z|[+-]\d{2}:?\d{2})$/i, '') // strip timezone + .replace(/(\d)(st|nd|rd|th)\b/gi, '$1') // strip ordinal suffixes + .replace(/,/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + // Handle month names (English and German) - convert to YYYY-MM-DD format + // Pattern: DD Month YYYY or DD. Month YYYY + var monthMatch = s.match(/^(\d{1,2})\.?\s+([a-zäö]+)\.?\s+(\d{4})(.*)$/i); + if (monthMatch) { + var day = parseInt(monthMatch[1], 10); + var monthNum = parseMonthName(monthMatch[2]); + var year = monthMatch[3]; + var rest = monthMatch[4] || ''; + if (monthNum) { + return year + '-' + pad2(monthNum) + '-' + pad2(day) + rest; + } + } + + // Pattern: Month DD, YYYY + monthMatch = s.match(/^([a-zäö]+)\.?\s+(\d{1,2})\s+(\d{4})(.*)$/i); + if (monthMatch) { + var monthNum = parseMonthName(monthMatch[1]); + var day = parseInt(monthMatch[2], 10); + var year = monthMatch[3]; + var rest = monthMatch[4] || ''; + if (monthNum) { + return year + '-' + pad2(monthNum) + '-' + pad2(day) + rest; + } + } + + // Pattern: YYYY Month DD + monthMatch = s.match(/^(\d{4})\s+([a-zäö]+)\.?\s+(\d{1,2})(.*)$/i); + if (monthMatch) { + var year = monthMatch[1]; + var monthNum = parseMonthName(monthMatch[2]); + var day = parseInt(monthMatch[3], 10); + var rest = monthMatch[4] || ''; + if (monthNum) { + return year + '-' + pad2(monthNum) + '-' + pad2(day) + rest; + } + } + + // DD.MM.YYYY or DD/MM/YYYY -> rearrange to YYYY-MM-DD for Date + var match = s.match(/^(\d{1,2})[-\/.](\d{1,2})[-\/.](\d{4})(.*)$/); + if (match) { + var a = parseInt(match[1], 10); + var b = parseInt(match[2], 10); + var year = match[3]; + var rest = match[4] || ''; + // Disambiguate: if first > 12, it must be day (DMY); else assume DMY + var day = a > 12 ? a : (b > 12 ? b : a); + var month = a > 12 ? b : (b > 12 ? a : b); + s = year + '-' + pad2(month) + '-' + pad2(day) + rest; + } + + return s; + } + + /** + * Try to parse a date/time string into a Date object. + * Returns null if parsing fails or produces an invalid date. + */ + function tryParse(input) { + if (!input || typeof input !== 'string') return null; + + var preprocessed = preprocess(input); + if (!preprocessed) return null; + + // Try native parsing + var ts = Date.parse(preprocessed); + if (!isNaN(ts)) return new Date(ts); + + // Fallback: replace dots/slashes with dashes and try again + var normalized = preprocessed.replace(/[\/\.]/g, '-'); + ts = Date.parse(normalized); + if (!isNaN(ts)) return new Date(ts); + + return null; + } + + /** + * Check if the original input contained a time component. + */ + function hasTimeComponent(input) { + return /\d{1,2}:\d{2}/.test(input); + } + + /** + * Format a Date object to YYYY-MM-DD or YYYY-MM-DD HH:MM:SS. + */ + function formatDate(date, includeTime) { + var out = date.getFullYear() + '-' + pad2(date.getMonth() + 1) + '-' + pad2(date.getDate()); + if (includeTime) { + out += ' ' + pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds()); + } + return out; + } + + /** + * Normalize a timestamp string to YYYY-MM-DD (or YYYY-MM-DD HH:MM:SS if time present). + * Returns null if the input cannot be parsed as a valid date. + */ + function normalizeTimestamp(input) { + var date = tryParse(input); + if (!date) return null; + return formatDate(date, hasTimeComponent(input)); + } + + function getEditor(edid) { + return document.getElementById(edid); + } + + function fixSelection(edid) { + if (typeof DWgetSelection !== 'function' || typeof pasteText !== 'function') return false; + var textarea = getEditor(edid); + if (!textarea) return false; + + var selection = DWgetSelection(textarea); + var text = selection.getText(); + if (!text || !text.trim()) return false; + + var normalized = normalizeTimestamp(text); + if (!normalized) return false; + + pasteText(selection, normalized, { nosel: true }); + return false; + } + + function fixAll(edid) { + var textarea = getEditor(edid); + if (!textarea) return false; + + var selection = typeof DWgetSelection === 'function' ? DWgetSelection(textarea) : null; + var original = textarea.value; + var replaced = original.replace(CANDIDATE_REGEX, function (match) { + var normalized = normalizeTimestamp(match); + return normalized || match; + }); + + if (replaced !== original) { + textarea.value = replaced; + if (selection && typeof DWsetSelection === 'function') { + selection.start = Math.min(selection.start, textarea.value.length); + selection.end = Math.min(selection.end, textarea.value.length); + DWsetSelection(selection); + } + } + + return false; + } + + DateFix.normalize = normalizeTimestamp; + DateFix.fixSelection = fixSelection; + DateFix.fixAll = fixAll; + + // Toolbar button action handlers + // DokuWiki toolbar looks for addBtnAction functions for custom button types. + // The buttons are registered via PHP (TOOLBAR_DEFINE hook in action.php). + window.addBtnActionLuxtoolsDatefix = function ($btn, props, edid) { + $btn.on('click', function () { + fixSelection(edid); + return false; + }); + return 'luxtools-datefix'; + }; + + window.addBtnActionLuxtoolsDatefixAll = function ($btn, props, edid) { + $btn.on('click', function () { + fixAll(edid); + return false; + }); + return 'luxtools-datefix-all'; + }; +})(); diff --git a/lang/en/lang.php b/lang/en/lang.php index 083dabb..9c42cb7 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -73,3 +73,5 @@ $lang["scratchpad_err_unreadable"] = "Scratchpad file is not readable"; $lang["toolbar_code_title"] = "Code Block"; $lang["toolbar_code_sample"] = "your code here"; +$lang["toolbar_datefix_title"] = "Date Fix"; +$lang["toolbar_datefix_all_title"] = "Date Fix (All)";