feat(docs): nested lists in HTML, update :help parser

- Docs HTML: improvements in https://github.com/neovim/tree-sitter-vimdoc
  allow us to many hacks in `gen_help_html.lua`.
- Docs HTML: support nested lists.
- Docs HTML: avoid extra newlines (too much whitespace) in old
  (preformatted) layout.
- Docs HTML: disable golden-grid for narrow viewport.
- Workaround for https://github.com/neovim/neovim/issues/20404

closes https://github.com/neovim/neovim/issues/20404
This commit is contained in:
Justin M. Keyes 2022-09-29 13:46:44 +02:00
parent 28fbdd3385
commit 088abbeb6e
10 changed files with 108 additions and 61 deletions

View File

@ -201,8 +201,8 @@ set(TREESITTER_LUA_SHA256 564594fe0ffd2f2fb3578a15019b723e1bc94ac82cb6a0103a6b3b
set(TREESITTER_VIM_URL https://github.com/vigoux/tree-sitter-viml/archive/v0.2.0.tar.gz) set(TREESITTER_VIM_URL https://github.com/vigoux/tree-sitter-viml/archive/v0.2.0.tar.gz)
set(TREESITTER_VIM_SHA256 608dcc31a7948cb66ae7f45494620e2e9face1af75598205541f80d782ec4501) set(TREESITTER_VIM_SHA256 608dcc31a7948cb66ae7f45494620e2e9face1af75598205541f80d782ec4501)
set(TREESITTER_HELP_URL https://github.com/neovim/tree-sitter-vimdoc/archive/v1.1.0.tar.gz) set(TREESITTER_HELP_URL https://github.com/neovim/tree-sitter-vimdoc/archive/c27e3e21a54f6d90dfb791f37d90eab5b28de971.tar.gz)
set(TREESITTER_HELP_SHA256 4c0ef80c6dc09acab362478950ec6be58a4ab1cbf2d95754b8fbb566e4c647a1) set(TREESITTER_HELP_SHA256 54a6a5b52a395097775f06f96ac1e1c9efdab10243550a467e1198a286b8c59c)
set(TREESITTER_URL https://github.com/tree-sitter/tree-sitter/archive/v0.20.7.tar.gz) set(TREESITTER_URL https://github.com/tree-sitter/tree-sitter/archive/v0.20.7.tar.gz)
set(TREESITTER_SHA256 b355e968ec2d0241bbd96748e00a9038f83968f85d822ecb9940cbe4c42e182e) set(TREESITTER_SHA256 b355e968ec2d0241bbd96748e00a9038f83968f85d822ecb9940cbe4c42e182e)

View File

@ -434,7 +434,7 @@ its config is non-empty: >
if vim.api.nvim_win_get_config(window_id).relative ~= '' then if vim.api.nvim_win_get_config(window_id).relative ~= '' then
-- window with this window_id is floating -- window with this window_id is floating
end end
> <
Buffer text can be highlighted by typical mechanisms (syntax highlighting, Buffer text can be highlighted by typical mechanisms (syntax highlighting,
|api-highlights|). The |hl-NormalFloat| group highlights normal text; |api-highlights|). The |hl-NormalFloat| group highlights normal text;
@ -456,7 +456,7 @@ Example: create a float with scratch buffer: >
let win = nvim_open_win(buf, 0, opts) let win = nvim_open_win(buf, 0, opts)
" optional: change highlight, otherwise Pmenu is used " optional: change highlight, otherwise Pmenu is used
call nvim_win_set_option(win, 'winhl', 'Normal:MyHighlight') call nvim_win_set_option(win, 'winhl', 'Normal:MyHighlight')
> <
============================================================================== ==============================================================================
Extended marks *api-extended-marks* *extmarks* Extended marks *api-extended-marks* *extmarks*
@ -500,8 +500,8 @@ Let's set an extmark at the first row (row=0) and third column (column=2).
01 2345678 01 2345678
0 ex|ample.. 0 ex|ample..
< ^ extmark position ^ extmark position
>
let g:mark_ns = nvim_create_namespace('myplugin') let g:mark_ns = nvim_create_namespace('myplugin')
let g:mark_id = nvim_buf_set_extmark(0, g:mark_ns, 0, 2, {}) let g:mark_id = nvim_buf_set_extmark(0, g:mark_ns, 0, 2, {})
< <
@ -520,8 +520,8 @@ use |nvim_buf_del_extmark()|. Deleting "x" in our example: >
0 12345678 0 12345678
0 e|ample.. 0 e|ample..
< ^ extmark position ^ extmark position
>
echo nvim_buf_get_extmark_by_id(0, g:mark_ns, g:mark_id, {}) echo nvim_buf_get_extmark_by_id(0, g:mark_ns, g:mark_id, {})
=> [0, 1] => [0, 1]
< <

View File

@ -7716,7 +7716,7 @@ sqrt({expr}) *sqrt()*
:echo sqrt(100) :echo sqrt(100)
< 10.0 > < 10.0 >
:echo sqrt(-4.01) :echo sqrt(-4.01)
< str2float('nan') < str2float("nan")
NaN may be different, it depends on system libraries. NaN may be different, it depends on system libraries.
Can also be used as a |method|: > Can also be used as a |method|: >

View File

@ -21,6 +21,7 @@ For a list of Vim variables see |vim-variable|.
tag char action in Insert mode ~ tag char action in Insert mode ~
----------------------------------------------------------------------- -----------------------------------------------------------------------
|i_CTRL-@| CTRL-@ insert previously inserted text and stop |i_CTRL-@| CTRL-@ insert previously inserted text and stop
insert insert
|i_CTRL-A| CTRL-A insert previously inserted text |i_CTRL-A| CTRL-A insert previously inserted text
@ -184,6 +185,7 @@ note: 1 = cursor movement command; 2 = can be undone/redone
tag char note action in Normal mode ~ tag char note action in Normal mode ~
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
CTRL-@ not used CTRL-@ not used
|CTRL-A| CTRL-A 2 add N to number at/after cursor |CTRL-A| CTRL-A 2 add N to number at/after cursor
|CTRL-B| CTRL-B 1 scroll N screens Backwards |CTRL-B| CTRL-B 1 scroll N screens Backwards
@ -469,6 +471,7 @@ These can be used after an operator or in Visual mode to select an object.
tag command action in op-pending and Visual mode ~ tag command action in op-pending and Visual mode ~
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
|v_aquote| a" double quoted string |v_aquote| a" double quoted string
|v_a'| a' single quoted string |v_a'| a' single quoted string
|v_a(| a( same as ab |v_a(| a( same as ab
@ -511,6 +514,7 @@ tag command action in op-pending and Visual mode ~
tag command action in Normal mode ~ tag command action in Normal mode ~
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
|CTRL-W_CTRL-B| CTRL-W CTRL-B same as "CTRL-W b" |CTRL-W_CTRL-B| CTRL-W CTRL-B same as "CTRL-W b"
|CTRL-W_CTRL-C| CTRL-W CTRL-C same as "CTRL-W c" |CTRL-W_CTRL-C| CTRL-W CTRL-C same as "CTRL-W c"
|CTRL-W_CTRL-D| CTRL-W CTRL-D same as "CTRL-W d" |CTRL-W_CTRL-D| CTRL-W CTRL-D same as "CTRL-W d"
@ -609,6 +613,7 @@ tag command action in Normal mode ~
tag char note action in Normal mode ~ tag char note action in Normal mode ~
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
|[_CTRL-D| [ CTRL-D jump to first #define found in current and |[_CTRL-D| [ CTRL-D jump to first #define found in current and
included files matching the word under the included files matching the word under the
cursor, start searching at beginning of cursor, start searching at beginning of
@ -699,6 +704,7 @@ tag char note action in Normal mode ~
tag char note action in Normal mode ~ tag char note action in Normal mode ~
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
g_CTRL-A g CTRL-A dump a memory profile g_CTRL-A g CTRL-A dump a memory profile
|g_CTRL-G| g CTRL-G show information about current cursor |g_CTRL-G| g CTRL-G show information about current cursor
position position
@ -802,6 +808,7 @@ g_CTRL-A g CTRL-A dump a memory profile
tag char note action in Normal mode ~ tag char note action in Normal mode ~
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
|z<CR>| z<CR> redraw, cursor line to top of window, |z<CR>| z<CR> redraw, cursor line to top of window,
cursor on first non-blank cursor on first non-blank
|zN<CR>| z{height}<CR> redraw, make window {height} lines high |zN<CR>| z{height}<CR> redraw, make window {height} lines high
@ -876,6 +883,7 @@ These can be used after an operator, but before a {motion} has been entered.
tag char action in Operator-pending mode ~ tag char action in Operator-pending mode ~
----------------------------------------------------------------------- -----------------------------------------------------------------------
|o_v| v force operator to work charwise |o_v| v force operator to work charwise
|o_V| V force operator to work linewise |o_V| V force operator to work linewise
|o_CTRL-V| CTRL-V force operator to work blockwise |o_CTRL-V| CTRL-V force operator to work blockwise
@ -888,6 +896,7 @@ here are those that are different.
tag command note action in Visual mode ~ tag command note action in Visual mode ~
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
|v_CTRL-\_CTRL-N| CTRL-\ CTRL-N stop Visual mode |v_CTRL-\_CTRL-N| CTRL-\ CTRL-N stop Visual mode
|v_CTRL-\_CTRL-G| CTRL-\ CTRL-G go to Normal mode |v_CTRL-\_CTRL-G| CTRL-\ CTRL-G go to Normal mode
|v_CTRL-A| CTRL-A 2 add N to number in highlighted text |v_CTRL-A| CTRL-A 2 add N to number in highlighted text
@ -1008,6 +1017,7 @@ file names, tags, commands etc. as appropriate.
tag command action in Command-line editing mode ~ tag command action in Command-line editing mode ~
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
CTRL-@ not used CTRL-@ not used
|c_CTRL-A| CTRL-A do completion on the pattern in front of the |c_CTRL-A| CTRL-A do completion on the pattern in front of the
cursor and insert all matches cursor and insert all matches
@ -1118,6 +1128,7 @@ The commands are sorted on the non-optional part of their name.
tag command action ~ tag command action ~
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
|:| : nothing |:| : nothing
|:range| :{range} go to last line in {range} |:range| :{range} go to last line in {range}
|:!| :! filter lines or execute an external command |:!| :! filter lines or execute an external command

View File

@ -61,7 +61,7 @@ other words, this section describes which tokens are valid, how they can be
combined, and what their combinations mean. combined, and what their combinations mean.
The language constructs will be explained using the usual extended BNF The language constructs will be explained using the usual extended BNF
notation, in which { `a` } means 0 or more `a`'s, and [ `a` ] means an optional `a`. notation, in which `{ a }` means 0 or more `a`'s, and `[ a ]` means an optional `a`.
============================================================================== ==============================================================================
2.1 Lexical Conventions *luaref-langLexConv* 2.1 Lexical Conventions *luaref-langLexConv*

View File

@ -2782,7 +2782,7 @@ your browsing preferences. (see also: |netrw-settings|)
history are saved (as .netrwbook and history are saved (as .netrwbook and
.netrwhist). .netrwhist).
Netrw uses |expand()| on the string. Netrw uses |expand()| on the string.
default: stdpath('data') (see |stdpath()|) default: stdpath("data") (see |stdpath()|)
*g:netrw_keepdir* =1 (default) keep current directory immune from *g:netrw_keepdir* =1 (default) keep current directory immune from
the browsing directory. the browsing directory.

View File

@ -471,7 +471,7 @@ flag when defining the function, it is not relevant when executing it. >
:set cpo-=C :set cpo-=C
< <
*line-continuation-comment* *line-continuation-comment*
To add a comment in between the lines start with '"\ '. Notice the space To add a comment in between the lines start with `'"\ '`. Notice the space
after the backslash. Example: > after the backslash. Example: >
let array = [ let array = [
"\ first entry comment "\ first entry comment
@ -491,7 +491,7 @@ Rationale:
continuation lines to be part of the comment. Since it was like this continuation lines to be part of the comment. Since it was like this
for a long time, when making it possible to add a comment halfway a for a long time, when making it possible to add a comment halfway a
sequence of continuation lines, it was not possible to use \", since sequence of continuation lines, it was not possible to use \", since
that was a valid continuation line. Using '"\ ' comes closest, even that was a valid continuation line. Using `'"\ '` comes closest, even
though it may look a bit weird. Requiring the space after the though it may look a bit weird. Requiring the space after the
backslash is to make it very unlikely this is a normal comment line. backslash is to make it very unlikely this is a normal comment line.

View File

@ -1357,7 +1357,7 @@ LOG FILE *$NVIM_LOG_FILE* *E5430*
Besides 'debug' and 'verbose', Nvim keeps a general log file for internal Besides 'debug' and 'verbose', Nvim keeps a general log file for internal
debugging, plugins and RPC clients. > debugging, plugins and RPC clients. >
:echo $NVIM_LOG_FILE :echo $NVIM_LOG_FILE
By default, the file is located at stdpath('log')/log unless that path By default, the file is located at stdpath("log")/log unless that path
is inaccessible or if $NVIM_LOG_FILE was set before |startup|. is inaccessible or if $NVIM_LOG_FILE was set before |startup|.

View File

@ -67,7 +67,7 @@ local exclude_invalid = {
["v:_null_string"] = "builtin.txt", ["v:_null_string"] = "builtin.txt",
["vim.lsp.buf_request()"] = "lsp.txt", ["vim.lsp.buf_request()"] = "lsp.txt",
["vim.lsp.util.get_progress_messages()"] = "lsp.txt", ["vim.lsp.util.get_progress_messages()"] = "lsp.txt",
["vim.treesitter.start()"] = "treesitter.txt" ["vim.treesitter.start()"] = "treesitter.txt",
} }
local function tofile(fname, text) local function tofile(fname, text)
@ -96,11 +96,6 @@ local function url_encode(s)
'g') 'g')
end end
-- Removes the ">" and "<" chars that delineate a codeblock in Vim :help files.
local function trim_gt_lt(s)
return s:gsub('^%s*>%s*\n', ''):gsub('\n<', '')
end
local function expandtabs(s) local function expandtabs(s)
return s:gsub('\t', (' '):rep(8)) return s:gsub('\t', (' '):rep(8))
end end
@ -123,15 +118,29 @@ local function basename_noext(f)
end end
local function is_blank(s) local function is_blank(s)
return not not s:find('^%s*$') return not not s:find([[^[\t ]*$]])
end end
local function trim(s) local function trim(s, dir)
return vim.trim(s) return vim.fn.trim(s, '\r\t\n ', dir or 0)
end end
local function trim_bullet(s) -- Remove common punctuation from URLs.
return s:gsub('^%s*[-*•]%s', '') --
-- TODO: fix this in the parser instead... https://github.com/neovim/tree-sitter-vimdoc
--
-- @returns (fixed_url, removed_chars) where `removed_chars` is in the order found in the input.
local function fix_url(url)
local removed_chars = ''
local fixed_url = url
-- Remove up to one of each char from end of the URL, in this order.
for _, c in ipairs({ '.', ')', }) do
if fixed_url:sub(-1) == c then
removed_chars = c .. removed_chars
fixed_url = fixed_url:sub(1, -2)
end
end
return fixed_url, removed_chars
end end
-- Checks if a given line is a "noise" line that doesn't look good in HTML form. -- Checks if a given line is a "noise" line that doesn't look good in HTML form.
@ -357,7 +366,8 @@ local function visit_node(root, level, lang_tree, headings, opt, stats)
if node_name == 'help_file' then -- root node if node_name == 'help_file' then -- root node
return text return text
elseif node_name == 'url' then elseif node_name == 'url' then
return ('%s<a href="%s">%s</a>'):format(ws(), trimmed, trimmed) local fixed_url, removed_chars = fix_url(trimmed)
return ('%s<a href="%s">%s</a>%s'):format(ws(), fixed_url, fixed_url, removed_chars)
elseif node_name == 'word' or node_name == 'uppercase_name' then elseif node_name == 'word' or node_name == 'uppercase_name' then
return html_esc(text) return html_esc(text)
elseif node_name == 'h1' or node_name == 'h2' or node_name == 'h3' then elseif node_name == 'h1' or node_name == 'h2' or node_name == 'h3' then
@ -383,38 +393,37 @@ local function visit_node(root, level, lang_tree, headings, opt, stats)
return '' return ''
end end
if opt.old then if opt.old then
-- XXX: Treat old docs as preformatted; random indentation is used for layout there. -- XXX: Treat "old" docs as preformatted: they use indentation for layout.
return ('<div class="old-help-para">%s</div>\n'):format(text) -- Trim trailing newlines to avoid too much whitespace between divs.
return ('<div class="old-help-para">%s</div>\n'):format(trim(text, 2))
end end
return string.format('<div class="help-para">\n%s\n</div>\n', text) return string.format('<div class="help-para">\n%s\n</div>\n', text)
elseif node_name == 'line' then elseif node_name == 'line' then
local sib = root:prev_sibling() if parent ~= 'codeblock' and (is_blank(text) or is_noise(text, stats.noise_lines)) then
local sib_last = sib and sib:named_child(sib:named_child_count() - 1) return '' -- Discard common "noise" lines.
local in_li = false
-- XXX: parser bug: (codeblock) without terminating "<" consumes first char of the next (line). Recover it here.
local recovered = (sib_last and sib_last:type() == 'codeblock') and node_text(root:prev_sibling()):sub(-1) or ''
recovered = recovered == '<' and '' or html_esc(recovered)
-- XXX: see if we are currently "in" a listitem.
while sib ~= nil and not in_li do
in_li = (sib:type() == 'line_li')
sib = sib:prev_sibling()
end end
-- XXX: Avoid newlines (too much whitespace) after block elements in old (preformatted) layout.
-- Close the current listitem. local div = opt.old and root:child(0) and vim.tbl_contains({'column_heading', 'h1', 'h2', 'h3'}, root:child(0):type())
local close = (in_li and next_ ~= 'line') and '</div>' or '' return string.format('%s%s', div and trim(text) or text, div and '' or '\n')
if is_blank(text) or is_noise(text, stats.noise_lines) then
return close -- Discard common "noise" lines.
end
local div = (root:child(0) and root:child(0):type() == 'column_heading') or close ~= ''
return string.format('%s%s%s%s', recovered, div and trim(text) or text, div and '' or '\n', close)
elseif node_name == 'line_li' then elseif node_name == 'line_li' then
-- Close the listitem immediately if the next sibling is not a line. local sib = root:prev_sibling()
local close = (next_ ~= 'line') and '</div>' or '' local prev_li = sib and sib:type() == 'line_li'
return string.format('<div class="help-li">%s %s', trim_bullet(text), close)
if not prev_li then
opt.indent = 1
else
-- The previous listitem _sibling_ is _logically_ the _parent_ if it is indented less.
local parent_indent = get_indent(node_text(sib))
local this_indent = get_indent(node_text())
if this_indent > parent_indent then
opt.indent = opt.indent + 1
elseif this_indent < parent_indent then
opt.indent = math.max(1, opt.indent - 1)
end
end
local margin = opt.indent == 1 and '' or ('margin-left: %drem;'):format((1.5 * opt.indent))
return string.format('<div class="help-li" style="%s">%s</div>', margin, text)
elseif node_name == 'taglink' or node_name == 'optionlink' then elseif node_name == 'taglink' or node_name == 'optionlink' then
if root:has_error() then if root:has_error() then
return text return text
@ -429,7 +438,10 @@ local function visit_node(root, level, lang_tree, headings, opt, stats)
elseif node_name == 'argument' then elseif node_name == 'argument' then
return ('%s<code>{%s}</code>'):format(ws(), text) return ('%s<code>{%s}</code>'):format(ws(), text)
elseif node_name == 'codeblock' then elseif node_name == 'codeblock' then
return ('<pre>%s</pre>'):format(html_esc(trim_indent(trim_gt_lt(text)))) if is_blank(text) then
return ''
end
return ('<pre>%s</pre>'):format(html_esc(trim(trim_indent(text), 2)))
elseif node_name == 'tag' then -- anchor elseif node_name == 'tag' then -- anchor
if root:has_error() then if root:has_error() then
return text return text
@ -630,7 +642,9 @@ local function gen_one(fname, to_fname, old, commit)
local main = '' local main = ''
for _, tree in ipairs(lang_tree:trees()) do for _, tree in ipairs(lang_tree:trees()) do
main = main .. (visit_node(tree:root(), 0, tree, headings, { buf = buf, old = old, fname = fname, to_fname = to_fname }, stats)) main = main .. (visit_node(tree:root(), 0, tree, headings,
{ buf = buf, old = old, fname = fname, to_fname = to_fname, indent = 1, },
stats))
end end
main = ([[ main = ([[
@ -718,6 +732,17 @@ local function gen_css(fname)
position: fixed; position: fixed;
left: 67%; left: 67%;
} }
.golden-grid {
display: grid;
grid-template-columns: 65% auto;
grid-gap: 1em;
}
}
@media (max-width: 40em) {
.golden-grid {
/* Disable grid for narrow viewport (mobile phone). */
display: block;
}
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
@ -831,11 +856,6 @@ local function gen_css(fname)
color: gray; color: gray;
font-size: smaller; font-size: smaller;
} }
.golden-grid {
display: grid;
grid-template-columns: 65% auto;
grid-gap: 1em;
}
]] ]]
tofile(fname, css) tofile(fname, css)
end end
@ -869,6 +889,22 @@ function M._test()
eq(5, get_indent(' a\n \n b\n c\n d\n e\n')) eq(5, get_indent(' a\n \n b\n c\n d\n e\n'))
eq('a\n \n b\n c\n d\n e\n', trim_indent(' a\n \n b\n c\n d\n e\n')) eq('a\n \n b\n c\n d\n e\n', trim_indent(' a\n \n b\n c\n d\n e\n'))
local fixed_url, removed_chars = fix_url('https://example.com).')
eq('https://example.com', fixed_url)
eq(').', removed_chars)
fixed_url, removed_chars = fix_url('https://example.com.)')
eq('https://example.com.', fixed_url)
eq(')', removed_chars)
fixed_url, removed_chars = fix_url('https://example.com.')
eq('https://example.com', fixed_url)
eq('.', removed_chars)
fixed_url, removed_chars = fix_url('https://example.com)')
eq('https://example.com', fixed_url)
eq(')', removed_chars)
fixed_url, removed_chars = fix_url('https://example.com')
eq('https://example.com', fixed_url)
eq('', removed_chars)
print('all tests passed') print('all tests passed')
end end

View File

@ -21,7 +21,7 @@ describe(':help docs', function()
ok(rv.helpfiles > 100, '>100 :help files', rv.helpfiles) ok(rv.helpfiles > 100, '>100 :help files', rv.helpfiles)
-- Check that parse errors did not increase wildly. -- Check that parse errors did not increase wildly.
-- TODO: Fix all parse errors in :help files. -- TODO: Fix all parse errors in :help files.
ok(rv.err_count < 250, '<250 parse errors', rv.err_count) ok(rv.err_count < 280, '<280 parse errors', rv.err_count)
eq({}, rv.invalid_links, exec_lua([[return 'found invalid :help tag links:\n'..vim.inspect(...)]], rv.invalid_links)) eq({}, rv.invalid_links, exec_lua([[return 'found invalid :help tag links:\n'..vim.inspect(...)]], rv.invalid_links))
end) end)