Add date fixer functions

This commit is contained in:
2026-01-30 11:12:50 +01:00
parent 47889c7d4c
commit 487e96b588
7 changed files with 305 additions and 12 deletions

View File

@@ -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:

View File

@@ -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" => "</code>",
"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,
];
}
}

View File

@@ -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."

10
images/date-fix-all.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
<rect x="2" y="3" width="12" height="11" rx="1" ry="1" fill="none" stroke="#000" stroke-width="1" />
<rect x="2" y="5" width="12" height="2" fill="#000" />
<rect x="4" y="1" width="2" height="4" fill="#000" />
<rect x="10" y="1" width="2" height="4" fill="#000" />
<path d="M4 9h8" stroke="#000" stroke-width="1" />
<path d="M4 11h8" stroke="#000" stroke-width="1" />
<circle cx="13" cy="13" r="2.2" fill="#000" />
<path d="M12.2 13l0.6 0.6 1-1" stroke="#fff" stroke-width="1" fill="none" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 665 B

8
images/date-fix.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
<rect x="2" y="3" width="12" height="11" rx="1" ry="1" fill="none" stroke="#000" stroke-width="1" />
<rect x="2" y="5" width="12" height="2" fill="#000" />
<rect x="4" y="1" width="2" height="4" fill="#000" />
<rect x="10" y="1" width="2" height="4" fill="#000" />
<path d="M5 9h6" stroke="#000" stroke-width="1" />
<path d="M5 11h4" stroke="#000" stroke-width="1" />
</svg>

After

Width:  |  Height:  |  Size: 490 B

235
js/date-fix.js Normal file
View File

@@ -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<Type> 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';
};
})();

View File

@@ -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)";