fix(client): colour-span conceal must skip unallocatable colours
CI / cargo fmt --check (push) Successful in 34s
CI / cargo clippy (push) Successful in 42s
CI / cargo test (push) Successful in 35s
CI / editor keymaps (push) Successful in 1m36s

A literal `<span style="color:…">` in prose (the sample wiki's own colorize
instructions used one, with a `…` ellipsis as the colour) was matched by the
scanner and fed to `highlight guifg=…`, which raised E254 on the un-silenced
guifg line. That aborted the whole nuwiki#colors#refresh(), so on the Vim
clients NO spans got concealed (start-vim) and follow-link crashed mid
FileType autocmd (start-vim-coc). Neovim happened to dodge it depending on
which buffer was scanned.

s:define() now:
- validates the colour (only #hex or an alphabetic name) and skips anything
  else — rgb()/hsl() functions and prose examples no longer create bogus
  conceal regions or errors;
- runs both `highlight` calls under `silent!`, so a valid-format but unknown
  name can't abort the refresh either (tags still conceal, text just isn't
  recoloured).

Also reworded the sample wiki's colorize note so it no longer embeds a
literal span tag.

Test: colorize.skips_unallocatable_colour (vim) — an rgb() span before a real
one: refresh doesn't error, the rgb span isn't concealed, the real span still
is. nvim 269, vim 260+18, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 16:09:15 +00:00
parent 15ba774a28
commit 9de8543385
3 changed files with 32 additions and 6 deletions
+11 -3
View File
@@ -41,6 +41,13 @@ function! s:define(color) abort
return return
endif endif
let b:nuwiki_color_seen[a:color] = 1 let b:nuwiki_color_seen[a:color] = 1
" Only handle colours Vim can actually allocate: a #hex value or an
" alphabetic name. Anything else — rgb()/hsl() functions, or a literal
" `<span style="color:…">` that turns up in prose/examples — is skipped, so
" a stray match can't create a bogus conceal region or abort the refresh.
if a:color !~# '^#\x\{3,8}$' && a:color !~# '^[A-Za-z][A-Za-z0-9]*$'
return
endif
let l:grp = 'nuwikiColorSpan_' . substitute(a:color, '[^A-Za-z0-9]', '_', 'g') let l:grp = 'nuwikiColorSpan_' . substitute(a:color, '[^A-Za-z0-9]', '_', 'g')
let l:esc = escape(a:color, '/\.*$^~[]') let l:esc = escape(a:color, '/\.*$^~[]')
" Clear first so re-runs / load-order never stack duplicate regions. " Clear first so re-runs / load-order never stack duplicate regions.
@@ -52,9 +59,10 @@ function! s:define(color) abort
\ . ' start=/<span style="color:' . l:esc . '">/' \ . ' start=/<span style="color:' . l:esc . '">/'
\ . ' end=#</span>#' \ . ' end=#</span>#'
\ . ' oneline keepend concealends' \ . ' oneline keepend concealends'
" guifg works for names and #hex under 'termguicolors'. ctermfg only accepts " guifg works for names and #hex under 'termguicolors'; ctermfg only for
" named colours, so try it for non-hex and swallow E254 for unknown names. " named colours. Both are best-effort — `silent!` swallows E254 for a name
execute 'highlight default ' . l:grp . ' guifg=' . a:color " Vim doesn't know (the tags still conceal, the text just isn't recoloured).
silent! execute 'highlight default ' . l:grp . ' guifg=' . a:color
if a:color !~# '^#' if a:color !~# '^#'
silent! execute 'highlight ' . l:grp . ' ctermfg=' . a:color silent! execute 'highlight ' . l:grp . ' ctermfg=' . a:color
endif endif
+3 -3
View File
@@ -97,9 +97,9 @@ super^script^ and sub,,script,,, and inline math $e^{i\pi} + 1 = 0$.
Put the cursor on a word and run `:VimwikiColorize red` (or press Put the cursor on a word and run `:VimwikiColorize red` (or press
`<Leader>wc` and type a colour); or visually select a phrase and press `<Leader>wc` and type a colour); or visually select a phrase and press
`<Leader>wc`. Each wraps the target in `<span style="color:…">…</span>` — `<Leader>wc`. Each wraps the target in an inline colour-span tag whose
the tags are concealed, so you see just the word painted in that colour markup is concealed, so you see just the word painted in that colour (move
(move the cursor onto it to reveal the raw markup). the cursor onto it to reveal the raw markup).
- colorize this important word - colorize this important word
- now select and colour this whole phrase - now select and colour this whole phrase
+18
View File
@@ -545,6 +545,24 @@ call s:record(
\ 'concealed=' . synconcealed(1, 1)[0] \ 'concealed=' . synconcealed(1, 1)[0]
\ . ' grp=' . synIDattr(synID(1, stridx(s:cz_l2, 'target') + 1, 1), 'name')) \ . ' grp=' . synIDattr(synID(1, stridx(s:cz_l2, 'target') + 1, 1), 'name'))
" A span whose colour Vim can't allocate (rgb()/prose) must be skipped — not
" abort refresh, not conceal — while a real span afterwards still conceals.
call s:set_buf(['plain', 'x <span style="color:rgb(1,2,3)">y</span> z', '<span style="color:red">word</span> z'])
call cursor(1, 1)
let s:cz_e = ''
try
call nuwiki#colors#refresh()
catch
let s:cz_e = v:exception
endtry
call s:record(
\ s:cz_e ==# ''
\ && synconcealed(3, 1)[0] == 1
\ && synconcealed(2, stridx(getline(2), '<span') + 1)[0] == 0 ? 1 : 0,
\ 'colorize.skips_unallocatable_colour',
\ 'err=' . s:cz_e . ' real=' . synconcealed(3, 1)[0]
\ . ' rgb=' . synconcealed(2, stridx(getline(2), '<span') + 1)[0])
" ===== Follow-link resolves + opens in the current window (regression) ===== " ===== Follow-link resolves + opens in the current window (regression) =====
" follow_link_or_create() must resolve the definition itself and `:edit` the " follow_link_or_create() must resolve the definition itself and `:edit` the
" target in the CURRENT window — including a page that does not exist yet " target in the CURRENT window — including a page that does not exist yet