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;
      display: block;
    }
    .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 {
      white-space: normal;
      display: flex;
      flex-flow: row wrap;
      justify-content: space-between;
      gap: 0 15px;
    }
    /* 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 {
      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.fs.normalize('$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.fs.normalize(d)) == 1
  end, 'valid directory')
  vim.validate('to_dir', to_dir, 'string')
  vim.validate('include', include, 'table', true)
  vim.validate('commit', commit, 'string', true)
  vim.validate('parser_path', parser_path, function(f)
    return vim.fn.filereadable(vim.fs.normalize(f)) == 1
  end, true, 'valid vimdoc.{so,dll} filepath')
  local err_count = 0
  local redirects_count = 0
  ensure_runtimepath()
  tagmap = get_helptags(vim.fs.normalize(help_dir))
  helpfiles = get_helpfiles(help_dir, include)
  to_dir = vim.fs.normalize(to_dir)
  parser_path = parser_path and vim.fs.normalize(parser_path) or nil
  print(('output dir: %s\n\n'):format(to_dir))
  vim.fn.mkdir(to_dir, 'p')
  gen_css(('%s/help.css'):format(to_dir))
  for _, f in ipairs(helpfiles) do
    -- "foo.txt"
    local helpfile = vim.fs.basename(f)
    -- "to/dir/foo.html"
    local to_fname = ('%s/%s'):format(to_dir, get_helppage(helpfile))
    local html, stats =
      gen_one(f, nil, to_fname, not new_layout[helpfile], commit or '?', parser_path)
    tofile(to_fname, html)
    print(
      ('generated (%-2s errors): %-15s => %s'):format(
        #stats.parse_errors,
        helpfile,
        vim.fs.basename(to_fname)
      )
    )
    -- Generate redirect pages for renamed help files.
    local helpfile_tag = (helpfile:gsub('%.txt$', ''))
    local redirect_from = redirects[helpfile_tag]
    if redirect_from then
      local redirect_text = ([[
*%s*      Nvim
This document moved to: |%s|
==============================================================================
This document moved to: |%s|
This document moved to: |%s|
==============================================================================
 vim:tw=78:ts=8:ft=help:norl:
      ]]):format(
        redirect_from,
        helpfile_tag,
        helpfile_tag,
        helpfile_tag,
        helpfile_tag,
        helpfile_tag
      )
      local redirect_to = ('%s/%s'):format(to_dir, get_helppage(redirect_from))
      local redirect_html, _ =
        gen_one(redirect_from, redirect_text, redirect_to, false, commit or '?', parser_path)
      assert(redirect_html:find(helpfile_tag))
      tofile(redirect_to, redirect_html)
      print(
        ('generated (redirect) : %-15s => %s'):format(
          redirect_from .. '.txt',
          vim.fs.basename(to_fname)
        )
      )
      redirects_count = redirects_count + 1
    end
    err_count = err_count + #stats.parse_errors
  end
  print(('\ngenerated %d html pages'):format(#helpfiles + redirects_count))
  print(('total errors: %d'):format(err_count))
  print(('invalid tags: %s'):format(vim.inspect(invalid_links)))
  assert(#(include or {}) > 0 or redirects_count == vim.tbl_count(redirects)) -- sanity check
  print(('redirects: %d'):format(redirects_count))
  print('\n')
  --- @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.fs.normalize(d)) == 1
  end, 'valid directory')
  vim.validate('include', include, 'table', true)
  vim.validate('parser_path', parser_path, function(f)
    return vim.fn.filereadable(vim.fs.normalize(f)) == 1
  end, true, 'valid vimdoc.{so,dll} filepath')
  local err_count = 0 ---@type integer
  local files_to_errors = {} ---@type table
  ensure_runtimepath()
  tagmap = get_helptags(vim.fs.normalize(help_dir))
  helpfiles = get_helpfiles(help_dir, include)
  parser_path = parser_path and vim.fs.normalize(parser_path) or nil
  for _, f in ipairs(helpfiles) do
    local helpfile = 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.fs.normalize(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 = vim.fs.dirname(vim.fn.tempname())
  help_dir = vim.fs.normalize(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