Add date fixer functions
This commit is contained in:
16
README.md
16
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:
|
||||
|
||||
18
action.php
18
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" => "</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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
28
deploy.sh
28
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."
|
||||
|
||||
10
images/date-fix-all.svg
Normal file
10
images/date-fix-all.svg
Normal 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
8
images/date-fix.svg
Normal 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
235
js/date-fix.js
Normal 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';
|
||||
};
|
||||
})();
|
||||
@@ -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)";
|
||||
|
||||
Reference in New Issue
Block a user