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

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