diff --git a/backend/utils.py b/backend/utils.py index 06a37ac..795aaf0 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -964,31 +964,64 @@ def get_template_content(notes_dir: str, template_name: str) -> Optional[str]: return None +# Matches custom strftime placeholders like {{date:%Y%m%d}} or {{time:%H%M%S}}. +# The three prefixes mirror the bare {{date}}, {{time}}, {{datetime}} placeholders; +# disallowing '{' and '}' inside the format keeps parsing unambiguous and +# guarantees we never swallow an adjacent placeholder. +_STRFTIME_PLACEHOLDER_RE = re.compile(r'\{\{(date|time|datetime):([^{}]+)\}\}') + + def apply_template_placeholders(content: str, note_path: str) -> str: """ Replace template placeholders with actual values. - - Supported placeholders: + + Built-in named placeholders (fixed default formats): {{date}} - Current date (YYYY-MM-DD) {{time}} - Current time (HH:MM:SS) {{datetime}} - Current datetime (YYYY-MM-DD HH:MM:SS) - {{timestamp}} - Unix timestamp + {{timestamp}} - Unix timestamp (seconds) {{year}} - Current year (YYYY) {{month}} - Current month (MM) {{day}} - Current day (DD) {{title}} - Note name without extension {{folder}} - Parent folder name - + + Custom date/time formats (strftime escape hatch): + {{date:FMT}}, {{time:FMT}}, {{datetime:FMT}} + + Any of the three date/time placeholders above also accepts an + optional ":FMT" suffix where FMT is a Python strftime() format + string. Pick the prefix whose default format covers the same + components you want to format. Examples: + + {{datetime:%Y%m%d%H%M%S}} -> 20260506154200 (filename-safe stamp) + {{date:%d/%m/%Y}} -> 06/05/2026 (European date) + {{date:%A}} -> Wednesday (full weekday name) + {{time:%H%M%S}} -> 154200 (compact time) + {{date:%V}} -> 19 (ISO week number) + + Invalid format strings are left in the output unchanged so a typo + is visible rather than silently swallowed. + Args: content: Template content with placeholders note_path: Path of the note being created - + Returns: Content with placeholders replaced """ now = datetime.now() note = Path(note_path) - + + def _strftime_sub(match: 're.Match[str]') -> str: + fmt = match.group(2) + try: + return now.strftime(fmt) + except (ValueError, TypeError): + return match.group(0) + + content = _STRFTIME_PLACEHOLDER_RE.sub(_strftime_sub, content) + replacements = { '{{date}}': now.strftime('%Y-%m-%d'), '{{time}}': now.strftime('%H:%M:%S'), @@ -1000,11 +1033,11 @@ def apply_template_placeholders(content: str, note_path: str) -> str: '{{title}}': note.stem, '{{folder}}': note.parent.name if str(note.parent) != '.' else 'Root', } - + result = content for placeholder, value in replacements.items(): result = result.replace(placeholder, value) - + return result diff --git a/documentation/FEATURES.md b/documentation/FEATURES.md index 4a3f738..6bb4b11 100644 --- a/documentation/FEATURES.md +++ b/documentation/FEATURES.md @@ -287,6 +287,7 @@ Create notes from reusable templates with dynamic placeholder replacement. - `{{day}}` - Current day (DD) - `{{title}}` - Note name (without extension) - `{{folder}}` - Parent folder name +- `{{date:FORMAT}}` / `{{time:FORMAT}}` / `{{datetime:FORMAT}}` - Custom format using any Python `strftime()` string, e.g. `{{datetime:%Y%m%d%H%M%S}}` → `20251126143045`. See [Templates](TEMPLATES.md#custom-datetime-formats) for the full recipe list. ### Example Template ```markdown diff --git a/documentation/TEMPLATES.md b/documentation/TEMPLATES.md index e6db71d..95fa266 100644 --- a/documentation/TEMPLATES.md +++ b/documentation/TEMPLATES.md @@ -46,6 +46,39 @@ Templates support dynamic placeholders that are replaced when you create a note: | `{{title}}` | Note name (without .md) | `Weekly Meeting` | | `{{folder}}` | Parent folder name | `Projects` | +### Custom date/time formats + +Need a different format than the defaults above? Any of the three +date/time placeholders — `{{date}}`, `{{time}}`, `{{datetime}}` — accepts +an optional `:FORMAT` suffix to override its default format. `FORMAT` is +any [Python `strftime()` format string](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes), +so anything `strftime` can do, your template can do. + +> **One rule to remember:** pick the prefix whose default already covers +> the components you want to format. Use `{{date:…}}` for date-only +> formats, `{{time:…}}` for time-only formats, and `{{datetime:…}}` when +> you need both. + +#### Common recipes + +| Template | Result | Use case | +|---|---|---| +| `{{date:%Y%m%d}}` | `20251126` | Date stamp for filenames | +| `{{time:%H%M%S}}` | `143045` | Time stamp for filenames | +| `{{datetime:%Y%m%d%H%M%S}}` | `20251126143045` | Sortable, filename-safe full stamp | +| `{{datetime:%Y%m%dT%H%M%S}}` | `20251126T143045` | ISO-8601 basic, no separators | +| `{{datetime:%Y-%m-%dT%H:%M:%S}}` | `2025-11-26T14:30:45` | ISO-8601 extended | +| `{{date:%d/%m/%Y}}` | `26/11/2025` | European-style date | +| `{{date:%m/%d/%Y}}` | `11/26/2025` | US-style date | +| `{{date:%A}}` | `Wednesday` | Full weekday name | +| `{{date:%a}}` | `Wed` | Short weekday name | +| `{{date:%B}}` | `November` | Full month name | +| `{{date:%V}}` | `48` | ISO week number | +| `{{date:%j}}` | `330` | Day of year (1–366) | + +> Invalid format strings (typos) are left in the output unchanged so the +> mistake is visible rather than silently swallowed. + ### Example Template ```markdown diff --git a/frontend/index.html b/frontend/index.html index acdb043..86c66de 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -703,17 +703,17 @@ to { opacity: 1; } } - /* Syntax highlight colors using theme variables */ - .syntax-overlay .md-heading { color: var(--accent-primary); font-weight: 600; } - .syntax-overlay .md-bold { color: var(--text-primary); font-weight: 700; } - .syntax-overlay .md-italic { color: var(--text-secondary); font-style: italic; } - .syntax-overlay .md-code { color: var(--accent-secondary, var(--accent-primary)); background: var(--bg-tertiary); border-radius: 2px; } - .syntax-overlay .md-link { color: var(--link-color, var(--accent-primary)); } - .syntax-overlay .md-link-url { color: var(--text-tertiary); } - .syntax-overlay .md-list { color: var(--accent-primary); } - .syntax-overlay .md-blockquote { color: var(--text-tertiary); font-style: italic; } - .syntax-overlay .md-hr { color: var(--border-primary); } - .syntax-overlay .md-wikilink { color: var(--link-internal, var(--accent-primary)); } + /* Syntax highlight colors (color-only — see issue #212) */ + .syntax-overlay .md-heading { color: var(--accent-primary); } + .syntax-overlay .md-bold { color: var(--syntax-bold, #fbbf24); } + .syntax-overlay .md-italic { color: var(--syntax-italic, #a78bfa); } + .syntax-overlay .md-code { color: var(--accent-secondary, var(--accent-primary)); background: var(--bg-tertiary); border-radius: 2px; } + .syntax-overlay .md-link { color: var(--link-color, var(--accent-primary)); } + .syntax-overlay .md-link-url { color: var(--text-tertiary); } + .syntax-overlay .md-list { color: var(--accent-primary); } + .syntax-overlay .md-blockquote{ color: var(--text-tertiary); } + .syntax-overlay .md-hr { color: var(--border-primary); } + .syntax-overlay .md-wikilink { color: var(--link-internal, var(--accent-primary)); } .syntax-overlay .md-frontmatter { color: var(--text-tertiary); } .syntax-overlay .md-codeblock { color: var(--text-secondary); background: var(--bg-tertiary); } diff --git a/themes/cobalt2.css b/themes/cobalt2.css index 007c2af..cf9b2e6 100644 --- a/themes/cobalt2.css +++ b/themes/cobalt2.css @@ -28,7 +28,13 @@ --success: #3ad900; --error: #ff0088; --warning: #ffc600; - + + /* Bold + italic — Cobalt2 native orange + cyan. + Heading is yellow (warm), so italic uses Cobalt2's signature cyan + instead of "lighter primary" to preserve warm/cool hierarchy. */ + --syntax-bold: #ff9d00; /* cobalt2 orange */ + --syntax-italic: #9effff; /* cobalt2 cyan */ + /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.4); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.5); diff --git a/themes/dark.css b/themes/dark.css index 9bfe12d..7b39b3b 100644 --- a/themes/dark.css +++ b/themes/dark.css @@ -26,7 +26,11 @@ --success: #34d399; --error: #f87171; --warning: #fbbf24; - + + /* Bold + italic — warm emphasis + lighter primary for italic (cohesive with heading) */ + --syntax-bold: #fbbf24; /* amber/yellow */ + --syntax-italic: #93c5fd; /* lighter blue — same family as heading, distinct by luminance */ + /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4); diff --git a/themes/dracula.css b/themes/dracula.css index dc2ca99..0e41bb7 100644 --- a/themes/dracula.css +++ b/themes/dracula.css @@ -28,7 +28,11 @@ --success: #50fa7b; --error: #ff5555; --warning: #f1fa8c; - + + /* Bold + italic — Dracula yellow + lighter purple (cohesive with purple heading) */ + --syntax-bold: #f1fa8c; /* Dracula yellow */ + --syntax-italic: #d8b4fe; /* lighter purple — same family as heading */ + /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4); diff --git a/themes/gruvbox-dark.css b/themes/gruvbox-dark.css index 2b99fab..2603f80 100644 --- a/themes/gruvbox-dark.css +++ b/themes/gruvbox-dark.css @@ -28,7 +28,11 @@ --success: #b8bb26; /* Green */ --error: #fb4934; /* Red */ --warning: #fabd2f; /* Yellow */ - + + /* Bold + italic — Gruvbox yellow + lighter peach (cohesive with orange heading) */ + --syntax-bold: #fabd2f; /* gruvbox yellow */ + --syntax-italic: #fdba74; /* lighter peach — same family as orange heading */ + /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.4); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.5); diff --git a/themes/hacker.css b/themes/hacker.css index 67af953..dc869c1 100644 --- a/themes/hacker.css +++ b/themes/hacker.css @@ -28,6 +28,10 @@ --error: #ff6b8a; --warning: #ffd447; + /* Bold + italic — amber CRT alert + lighter matrix green (same green family) */ + --syntax-bold: #ffd447; /* amber CRT alert (historically authentic — amber CRTs existed) */ + --syntax-italic: #86efac; /* lighter green — same family as matrix-green heading */ + /* Shadows — heavy, like a dim room */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.55); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.65); diff --git a/themes/light.css b/themes/light.css index 4451244..4233a07 100644 --- a/themes/light.css +++ b/themes/light.css @@ -26,7 +26,11 @@ --success: #10b981; --error: #ef4444; --warning: #f59e0b; - + + /* Bold + italic — warm emphasis + deeper primary for italic (cohesive on white) */ + --syntax-bold: #b45309; /* deep amber */ + --syntax-italic: #1d4ed8; /* deeper blue — same family as heading, distinct by depth */ + /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); diff --git a/themes/matcha-light.css b/themes/matcha-light.css index 13f2f94..6f5bdee 100644 --- a/themes/matcha-light.css +++ b/themes/matcha-light.css @@ -28,7 +28,11 @@ --success: #5da60a; --error: #d9534f; --warning: #f0ad4e; - + + /* Bold + italic — deep amber + deeper matcha green (cohesive with green heading on lime) */ + --syntax-bold: #a16207; /* deep amber — warm contrast on lime */ + --syntax-italic: #365314; /* deeper green — same family as matcha heading */ + /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(58, 77, 34, 0.1); --shadow-md: 0 4px 6px -1px rgba(58, 77, 34, 0.15); diff --git a/themes/monokai.css b/themes/monokai.css index de26aa0..124a1ab 100644 --- a/themes/monokai.css +++ b/themes/monokai.css @@ -28,7 +28,11 @@ --success: #a6e22e; --error: #f92672; --warning: #e6db74; - + + /* Bold + italic — Monokai yellow + lighter cyan (cohesive with cyan heading) */ + --syntax-bold: #e6db74; /* Monokai yellow (strings) */ + --syntax-italic: #a5f3fc; /* lighter cyan — same family as heading */ + /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4); diff --git a/themes/nord.css b/themes/nord.css index 7956a11..3fc9a4a 100644 --- a/themes/nord.css +++ b/themes/nord.css @@ -28,7 +28,11 @@ --success: #a3be8c; --error: #bf616a; --warning: #ebcb8b; - + + /* Bold + italic — aurora yellow + lighter frost (cohesive with frost-cyan heading) */ + --syntax-bold: #ebcb8b; /* nord13 — aurora yellow */ + --syntax-italic: #bae6fd; /* lighter frost — same family as heading, distinct by luminance */ + /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4); diff --git a/themes/ubuntu.css b/themes/ubuntu.css index b7c627f..cad9622 100644 --- a/themes/ubuntu.css +++ b/themes/ubuntu.css @@ -28,6 +28,10 @@ --error: #f07171; --warning: #ef7c2a; + /* Bold + italic — Ubuntu gold + lighter peach (cohesive with orange heading) */ + --syntax-bold: #ffce6b; /* warm gold */ + --syntax-italic: #fdba74; /* lighter peach — same family as orange heading */ + /* Shadows — widget.shadow */ --shadow-sm: 0 1px 2px 0 rgba(6, 1, 4, 0.55); --shadow-md: 0 4px 6px -1px rgba(6, 1, 4, 0.6); diff --git a/themes/vs-blue.css b/themes/vs-blue.css index ac3be2c..8e584c9 100644 --- a/themes/vs-blue.css +++ b/themes/vs-blue.css @@ -28,7 +28,11 @@ --success: #388a34; --error: #e51400; --warning: #f6a623; - + + /* Bold + italic — VS brown-amber + deeper VS blue (cohesive with blue heading on white) */ + --syntax-bold: #795e26; /* VS function brown-amber */ + --syntax-italic: #1e40af; /* deeper blue — same family as heading, distinct by depth */ + /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.08); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.12); diff --git a/themes/vue-high-contrast.css b/themes/vue-high-contrast.css index 1743e0d..cb39a66 100644 --- a/themes/vue-high-contrast.css +++ b/themes/vue-high-contrast.css @@ -28,7 +28,11 @@ --success: #4caf50; --error: #ff5252; --warning: #ffb74d; - + + /* Bold + italic — warm amber + lighter Vue green (cohesive with green heading) */ + --syntax-bold: #ffb74d; /* warm amber */ + --syntax-italic: #86efac; /* lighter green — same family as heading, distinct by luminance */ + /* Shadows - deeper for high contrast */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.6); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.7);