parse_errors: %d %s | noise_lines: %d
]]):format(
os.date('%Y-%m-%d %H:%M'),
commit,
commit:sub(1, 7),
#stats.parse_errors,
bug_link,
html_esc(table.concat(stats.noise_lines, '\n')),
#stats.noise_lines
)
html = ('%s%s%s
\n%s\n\n'):format(html, main, toc, footer)
vim.cmd('q!')
lang_tree:destroy()
return html, stats
end
local function gen_css(fname)
local css = [[
:root {
--code-color: #004b4b;
--tag-color: #095943;
}
@media (prefers-color-scheme: dark) {
:root {
--code-color: #00c243;
--tag-color: #00b7b7;
}
}
@media (min-width: 40em) {
.toc {
position: fixed;
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;
}
}
.toc {
/* max-width: 12rem; */
height: 85%; /* Scroll if there are too many items. https://github.com/neovim/neovim.github.io/issues/297 */
overflow: auto; /* Scroll if there are too many items. https://github.com/neovim/neovim.github.io/issues/297 */
}
.toc > div {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
html {
scroll-behavior: auto;
}
body {
font-size: 18px;
line-height: 1.5;
}
h1, h2, h3, h4, h5 {
font-family: sans-serif;
border-bottom: 1px solid var(--tag-color); /*rgba(0, 0, 0, .9);*/
}
h3, h4, h5 {
border-bottom-style: dashed;
}
.help-column_heading {
color: var(--code-color);
}
.help-body {
padding-bottom: 2em;
}
.help-line {
/* font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; */
}
.help-li {
white-space: normal;
display: list-item;
margin-left: 1.5rem; /* padding-left: 1rem; */
}
.help-para {
padding-top: 10px;
padding-bottom: 10px;
}
.old-help-para {
padding-top: 10px;
padding-bottom: 10px;
/* Tabs are used for alignment in old docs, so we must match Vim's 8-char expectation. */
tab-size: 8;
white-space: pre-wrap;
font-size: 16px;
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
word-wrap: break-word;
}
.old-help-para pre, .old-help-para pre:hover {
/* Text following
is already visually separated by the linebreak. */
margin-bottom: 0;
/* Long lines that exceed the textwidth should not be wrapped (no "pre-wrap").
Since text may overflow horizontally, we make the contents to be scrollable
(only if necessary) to prevent overlapping with the navigation bar at the right. */
white-space: pre;
overflow-x: auto;
}
/* TODO: should this rule be deleted? help tags are rendered as or , not */
a.help-tag, a.help-tag:focus, a.help-tag:hover {
color: inherit;
text-decoration: none;
}
.help-tag {
color: var(--tag-color);
}
/* Tag pseudo-header common in :help docs. */
.help-tag-right {
color: var(--tag-color);
margin-left: auto;
margin-right: 0;
float: right;
}
.help-tag a,
.help-tag-right a {
color: inherit;
}
.help-tag a:not(:hover),
.help-tag-right a:not(:hover) {
text-decoration: none;
}
h1 .help-tag, h2 .help-tag, h3 .help-tag {
font-size: smaller;
}
.help-heading {
overflow: hidden;
white-space: nowrap;
display: flex;
justify-content: space-between;
}
/* The (right-aligned) "tags" part of a section heading. */
.help-heading-tags {
margin-right: 10px;
}
.help-toc-h1 {
}
.help-toc-h2 {
margin-left: 1em;
}
.parse-error {
background-color: red;
}
.unknown-token {
color: black;
background-color: yellow;
}
code {
color: var(--code-color);
font-size: 16px;
}
pre {
/* Tabs are used in codeblocks only for indentation, not alignment, so we can aggressively shrink them. */
tab-size: 2;
white-space: pre-wrap;
line-height: 1.3; /* Important for ascii art. */
overflow: visible;
/* font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; */
font-size: 16px;
margin-top: 10px;
}
pre:last-child {
margin-bottom: 0;
}
pre:hover,
.help-heading:hover {
overflow: visible;
}
.generator-stats {
color: gray;
font-size: smaller;
}
]]
tofile(fname, css)
end
-- Testing
local function ok(cond, expected, actual, message)
assert(
(not expected and not actual) or (expected and actual),
'if "expected" is given, "actual" is also required'
)
if expected then
assert(
cond,
('%sexpected %s, got: %s'):format(
message and (message .. '\n') or '',
vim.inspect(expected),
vim.inspect(actual)
)
)
return cond
else
return assert(cond)
end
end
local function eq(expected, actual, message)
return ok(vim.deep_equal(expected, actual), expected, actual, message)
end
function M._test()
tagmap = get_helptags('$VIMRUNTIME/doc')
helpfiles = get_helpfiles(vim.fn.expand('$VIMRUNTIME/doc'))
ok(vim.tbl_count(tagmap) > 3000, '>3000', vim.tbl_count(tagmap))
ok(
vim.endswith(tagmap['vim.diagnostic.set()'], 'diagnostic.txt'),
tagmap['vim.diagnostic.set()'],
'diagnostic.txt'
)
ok(vim.endswith(tagmap['%:s'], 'cmdline.txt'), tagmap['%:s'], 'cmdline.txt')
ok(is_noise([[vim:tw=78:isk=!-~,^*,^\|,^\":ts=8:noet:ft=help:norl:]]))
ok(is_noise([[ NVIM REFERENCE MANUAL by Thiago de Arruda ]]))
ok(not is_noise([[vim:tw=78]]))
eq(0, get_indent('a'))
eq(1, get_indent(' a'))
eq(2, get_indent(' a\n b\n c\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')
)
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.\n')
end
--- @class nvim.gen_help_html.gen_result
--- @field helpfiles string[] list of generated HTML files, from the source docs {include}
--- @field err_count integer number of parse errors in :help docs
--- @field invalid_links table
--- Generates HTML from :help docs located in `help_dir` and writes the result in `to_dir`.
---
--- Example:
---
--- gen('$VIMRUNTIME/doc', '/path/to/neovim.github.io/_site/doc/', {'api.txt', 'autocmd.txt', 'channel.txt'}, nil)
---
--- @param help_dir string Source directory containing the :help files. Must run `make helptags` first.
--- @param to_dir string Target directory where the .html files will be written.
--- @param include string[]|nil Process only these filenames. Example: {'api.txt', 'autocmd.txt', 'channel.txt'}
---
--- @return nvim.gen_help_html.gen_result result
function M.gen(help_dir, to_dir, include, commit, parser_path)
vim.validate {
help_dir = {
help_dir,
function(d)
return vim.fn.isdirectory(vim.fn.expand(d)) == 1
end,
'valid directory',
},
to_dir = { to_dir, 's' },
include = { include, 't', true },
commit = { commit, 's', true },
parser_path = {
parser_path,
function(f)
return f == nil or vim.fn.filereadable(vim.fn.expand(f)) == 1
end,
'valid vimdoc.{so,dll} filepath',
},
}
local err_count = 0
ensure_runtimepath()
tagmap = get_helptags(vim.fn.expand(help_dir))
helpfiles = get_helpfiles(help_dir, include)
to_dir = vim.fn.expand(to_dir)
parser_path = parser_path and vim.fn.expand(parser_path) or nil
print(('output dir: %s'):format(to_dir))
vim.fn.mkdir(to_dir, 'p')
gen_css(('%s/help.css'):format(to_dir))
for _, f in ipairs(helpfiles) do
local helpfile = vim.fs.basename(f)
local to_fname = ('%s/%s'):format(to_dir, get_helppage(helpfile))
local html, stats = gen_one(f, to_fname, not new_layout[helpfile], commit or '?', parser_path)
tofile(to_fname, html)
print(
('generated (%-4s errors): %-15s => %s'):format(
#stats.parse_errors,
helpfile,
vim.fs.basename(to_fname)
)
)
err_count = err_count + #stats.parse_errors
end
print(('generated %d html pages'):format(#helpfiles))
print(('total errors: %d'):format(err_count))
print(('invalid tags:\n%s'):format(vim.inspect(invalid_links)))
--- @type nvim.gen_help_html.gen_result
return {
helpfiles = helpfiles,
err_count = err_count,
invalid_links = invalid_links,
}
end
--- @class nvim.gen_help_html.validate_result
--- @field helpfiles integer number of generated helpfiles
--- @field err_count integer number of parse errors
--- @field parse_errors table
--- @field invalid_links table invalid tags in :help docs
--- @field invalid_urls table invalid URLs in :help docs
--- @field invalid_spelling table> invalid spelling in :help docs
--- Validates all :help files found in `help_dir`:
--- - checks that |tag| links point to valid helptags.
--- - recursively counts parse errors ("ERROR" nodes)
---
--- This is 10x faster than gen(), for use in CI.
---
--- @return nvim.gen_help_html.validate_result result
function M.validate(help_dir, include, parser_path)
vim.validate {
help_dir = {
help_dir,
function(d)
return vim.fn.isdirectory(vim.fn.expand(d)) == 1
end,
'valid directory',
},
include = { include, 't', true },
parser_path = {
parser_path,
function(f)
return f == nil or vim.fn.filereadable(vim.fn.expand(f)) == 1
end,
'valid vimdoc.{so,dll} filepath',
},
}
local err_count = 0 ---@type integer
local files_to_errors = {} ---@type table
ensure_runtimepath()
tagmap = get_helptags(vim.fn.expand(help_dir))
helpfiles = get_helpfiles(help_dir, include)
parser_path = parser_path and vim.fn.expand(parser_path) or nil
for _, f in ipairs(helpfiles) do
local helpfile = assert(vim.fs.basename(f))
local rv = validate_one(f, parser_path)
print(('validated (%-4s errors): %s'):format(#rv.parse_errors, helpfile))
if #rv.parse_errors > 0 then
files_to_errors[helpfile] = rv.parse_errors
vim.print(('%s'):format(vim.iter(rv.parse_errors):fold('', function(s, v)
return s .. '\n ' .. v
end)))
end
err_count = err_count + #rv.parse_errors
end
---@type nvim.gen_help_html.validate_result
return {
helpfiles = #helpfiles,
err_count = err_count,
parse_errors = files_to_errors,
invalid_links = invalid_links,
invalid_urls = invalid_urls,
invalid_spelling = invalid_spelling,
}
end
--- Validates vimdoc files on $VIMRUNTIME. and print human-readable error messages if fails.
---
--- If this fails, try these steps (in order):
--- 1. Fix/cleanup the :help docs.
--- 2. Fix the parser: https://github.com/neovim/tree-sitter-vimdoc
--- 3. File a parser bug, and adjust the tolerance of this test in the meantime.
---
--- @param help_dir? string e.g. '$VIMRUNTIME/doc' or './runtime/doc'
function M.run_validate(help_dir)
help_dir = vim.fn.expand(help_dir or '$VIMRUNTIME/doc')
print('doc path = ' .. vim.uv.fs_realpath(help_dir))
local rv = M.validate(help_dir)
-- Check that we actually found helpfiles.
ok(rv.helpfiles > 100, '>100 :help files', rv.helpfiles)
eq({}, rv.parse_errors, 'no parse errors')
eq(0, rv.err_count, 'no parse errors')
eq({}, rv.invalid_links, 'invalid tags in :help docs')
eq({}, rv.invalid_urls, 'invalid URLs in :help docs')
eq(
{},
rv.invalid_spelling,
'invalid spelling in :help docs (see spell_dict in scripts/gen_help_html.lua)'
)
end
--- Test-generates HTML from docs.
---
--- 1. Test that gen_help_html.lua actually works.
--- 2. Test that parse errors did not increase wildly. Because we explicitly test only a few
--- :help files, we can be precise about the tolerances here.
--- @param help_dir? string e.g. '$VIMRUNTIME/doc' or './runtime/doc'
function M.test_gen(help_dir)
local tmpdir = assert(vim.fs.dirname(vim.fn.tempname()))
help_dir = vim.fn.expand(help_dir or '$VIMRUNTIME/doc')
print('doc path = ' .. vim.uv.fs_realpath(help_dir))
-- Because gen() is slow (~30s), this test is limited to a few files.
local input = { 'help.txt', 'index.txt', 'nvim.txt' }
local rv = M.gen(help_dir, tmpdir, input)
eq(#input, #rv.helpfiles)
eq(0, rv.err_count, 'parse errors in :help docs')
eq({}, rv.invalid_links, 'invalid tags in :help docs')
end
return M