refactor(gen_vimdoc): use stronger typing for CONFIG, avoid dict

This commit is contained in:
Jongwook Choi 2023-12-28 13:04:34 -05:00
parent 5dc0bdfe98
commit 5e2d4b3c4d

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Generates Nvim :help docs from C/Lua docstrings, using Doxygen.
r"""Generates Nvim :help docs from C/Lua docstrings, using Doxygen.
Also generates *.mpack files. To inspect the *.mpack structure: Also generates *.mpack files. To inspect the *.mpack structure:
:new | put=v:lua.vim.inspect(v:lua.vim.mpack.decode(readfile('runtime/doc/api.mpack','B'))) :new | put=v:lua.vim.inspect(v:lua.vim.mpack.decode(readfile('runtime/doc/api.mpack','B')))
@ -32,24 +33,29 @@ The generated :help text for each function is formatted as follows:
parameter is marked as [out]. parameter is marked as [out].
- Each function documentation is separated by a single line. - Each function documentation is separated by a single line.
""" """
from __future__ import annotations
import argparse import argparse
import collections
import dataclasses
import logging
import os import os
import re import re
import sys
import shutil import shutil
import textwrap
import subprocess import subprocess
import collections import sys
import msgpack import textwrap
import logging
from typing import Tuple
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, List, Literal, Tuple
from xml.dom import minidom from xml.dom import minidom
import msgpack
Element = minidom.Element Element = minidom.Element
Document = minidom.Document Document = minidom.Document
MIN_PYTHON_VERSION = (3, 6) MIN_PYTHON_VERSION = (3, 7)
MIN_DOXYGEN_VERSION = (1, 9, 0) MIN_DOXYGEN_VERSION = (1, 9, 0)
if sys.version_info < MIN_PYTHON_VERSION: if sys.version_info < MIN_PYTHON_VERSION:
@ -68,7 +74,7 @@ if doxygen_version < MIN_DOXYGEN_VERSION:
# Need a `nvim` that supports `-l`, try the local build # Need a `nvim` that supports `-l`, try the local build
nvim_path = Path(__file__).parent / "../build/bin/nvim" nvim_path = Path(__file__).parent / "../build/bin/nvim"
if nvim_path.exists(): if nvim_path.exists():
nvim = str(nvim_path) nvim = nvim_path.resolve()
else: else:
# Until 0.9 is released, use this hacky way to check that "nvim -l foo.lua" works. # Until 0.9 is released, use this hacky way to check that "nvim -l foo.lua" works.
nvim_out = subprocess.check_output(['nvim', '-h'], universal_newlines=True) nvim_out = subprocess.check_output(['nvim', '-h'], universal_newlines=True)
@ -103,12 +109,59 @@ filter_cmd = '%s %s' % (sys.executable, script_path)
msgs = [] # Messages to show on exit. msgs = [] # Messages to show on exit.
lua2dox = os.path.join(base_dir, 'scripts', 'lua2dox.lua') lua2dox = os.path.join(base_dir, 'scripts', 'lua2dox.lua')
CONFIG = {
'api': { SectionName = str
'mode': 'c',
'filename': 'api.txt', @dataclasses.dataclass
class Config:
"""Config for documentation."""
mode: Literal['c', 'lua']
filename: str
"""Generated documentation target, e.g. api.txt"""
section_order: List[str]
"""Section ordering."""
files: List[str]
"""List of files/directories for doxygen to read, relative to `base_dir`."""
file_patterns: str
"""file patterns used by doxygen."""
section_name: Dict[str, SectionName]
"""Section name overrides. Key: filename (e.g., vim.c)"""
section_fmt: Callable[[SectionName], str]
"""For generated section names."""
helptag_fmt: Callable[[SectionName], str]
"""Section helptag."""
fn_helptag_fmt: Callable[[str, str, bool], str]
"""Per-function helptag."""
module_override: Dict[str, str]
"""Module name overrides (for Lua)."""
append_only: List[str]
"""Append the docs for these modules, do not start a new section."""
fn_name_prefix: str
"""Only function with this prefix are considered"""
fn_name_fmt: Callable[[str, str], str] | None = None
include_tables: bool = True
CONFIG: Dict[str, Config] = {
'api': Config(
mode = 'c',
filename = 'api.txt',
# Section ordering. # Section ordering.
'section_order': [ section_order=[
'vim.c', 'vim.c',
'vimscript.c', 'vimscript.c',
'command.c', 'command.c',
@ -121,31 +174,22 @@ CONFIG = {
'autocmd.c', 'autocmd.c',
'ui.c', 'ui.c',
], ],
# List of files/directories for doxygen to read, relative to `base_dir` files=['src/nvim/api'],
'files': ['src/nvim/api'], file_patterns = '*.h *.c',
# file patterns used by doxygen fn_name_prefix = 'nvim_',
'file_patterns': '*.h *.c', section_name={
# Only function with this prefix are considered
'fn_name_prefix': 'nvim_',
# Section name overrides.
'section_name': {
'vim.c': 'Global', 'vim.c': 'Global',
}, },
# For generated section names. section_fmt=lambda name: f'{name} Functions',
'section_fmt': lambda name: f'{name} Functions', helptag_fmt=lambda name: f'*api-{name.lower()}*',
# Section helptag. fn_helptag_fmt=lambda fstem, name, istbl: f'*{name}()*',
'helptag_fmt': lambda name: f'*api-{name.lower()}*', module_override={},
# Per-function helptag. append_only=[],
'fn_helptag_fmt': lambda fstem, name, istbl: f'*{name}()*', ),
# Module name overrides (for Lua). 'lua': Config(
'module_override': {}, mode='lua',
# Append the docs for these modules, do not start a new section. filename='lua.txt',
'append_only': [], section_order=[
},
'lua': {
'mode': 'lua',
'filename': 'lua.txt',
'section_order': [
'highlight.lua', 'highlight.lua',
'regex.lua', 'regex.lua',
'diff.lua', 'diff.lua',
@ -171,7 +215,7 @@ CONFIG = {
'snippet.lua', 'snippet.lua',
'text.lua', 'text.lua',
], ],
'files': [ files=[
'runtime/lua/vim/iter.lua', 'runtime/lua/vim/iter.lua',
'runtime/lua/vim/_editor.lua', 'runtime/lua/vim/_editor.lua',
'runtime/lua/vim/_options.lua', 'runtime/lua/vim/_options.lua',
@ -197,30 +241,30 @@ CONFIG = {
'runtime/lua/vim/_meta/regex.lua', 'runtime/lua/vim/_meta/regex.lua',
'runtime/lua/vim/_meta/spell.lua', 'runtime/lua/vim/_meta/spell.lua',
], ],
'file_patterns': '*.lua', file_patterns='*.lua',
'fn_name_prefix': '', fn_name_prefix='',
'fn_name_fmt': lambda fstem, name: ( fn_name_fmt=lambda fstem, name: (
name if fstem in [ 'vim.iter' ] else name if fstem in [ 'vim.iter' ] else
f'vim.{name}' if fstem in [ '_editor', 'vim.regex'] else f'vim.{name}' if fstem in [ '_editor', 'vim.regex'] else
f'vim.{name}' if fstem == '_options' and not name[0].isupper() else f'vim.{name}' if fstem == '_options' and not name[0].isupper() else
f'{fstem}.{name}' if fstem.startswith('vim') else f'{fstem}.{name}' if fstem.startswith('vim') else
name name
), ),
'section_name': { section_name={
'lsp.lua': 'core', 'lsp.lua': 'core',
'_inspector.lua': 'inspector', '_inspector.lua': 'inspector',
}, },
'section_fmt': lambda name: ( section_fmt=lambda name: (
'Lua module: vim' if name.lower() == '_editor' else 'Lua module: vim' if name.lower() == '_editor' else
'LUA-VIMSCRIPT BRIDGE' if name.lower() == '_options' else 'LUA-VIMSCRIPT BRIDGE' if name.lower() == '_options' else
f'VIM.{name.upper()}' if name.lower() in [ 'highlight', 'mpack', 'json', 'base64', 'diff', 'spell', 'regex' ] else f'VIM.{name.upper()}' if name.lower() in [ 'highlight', 'mpack', 'json', 'base64', 'diff', 'spell', 'regex' ] else
'VIM' if name.lower() == 'builtin' else 'VIM' if name.lower() == 'builtin' else
f'Lua module: vim.{name.lower()}'), f'Lua module: vim.{name.lower()}'),
'helptag_fmt': lambda name: ( helptag_fmt=lambda name: (
'*lua-vim*' if name.lower() == '_editor' else '*lua-vim*' if name.lower() == '_editor' else
'*lua-vimscript*' if name.lower() == '_options' else '*lua-vimscript*' if name.lower() == '_options' else
f'*vim.{name.lower()}*'), f'*vim.{name.lower()}*'),
'fn_helptag_fmt': lambda fstem, name, istbl: ( fn_helptag_fmt=lambda fstem, name, istbl: (
f'*vim.opt:{name.split(":")[-1]}()*' if ':' in name and name.startswith('Option') else f'*vim.opt:{name.split(":")[-1]}()*' if ':' in name and name.startswith('Option') else
# Exclude fstem for methods # Exclude fstem for methods
f'*{name}()*' if ':' in name else f'*{name}()*' if ':' in name else
@ -230,7 +274,7 @@ CONFIG = {
f'*{fstem}()*' if fstem.endswith('.' + name) else f'*{fstem}()*' if fstem.endswith('.' + name) else
f'*{fstem}.{name}{"" if istbl else "()"}*' f'*{fstem}.{name}{"" if istbl else "()"}*'
), ),
'module_override': { module_override={
# `shared` functions are exposed on the `vim` module. # `shared` functions are exposed on the `vim` module.
'shared': 'vim', 'shared': 'vim',
'_inspector': 'vim', '_inspector': 'vim',
@ -255,14 +299,14 @@ CONFIG = {
'text': 'vim.text', 'text': 'vim.text',
'glob': 'vim.glob', 'glob': 'vim.glob',
}, },
'append_only': [ append_only=[
'shared.lua', 'shared.lua',
], ],
}, ),
'lsp': { 'lsp': Config(
'mode': 'lua', mode='lua',
'filename': 'lsp.txt', filename='lsp.txt',
'section_order': [ section_order=[
'lsp.lua', 'lsp.lua',
'buf.lua', 'buf.lua',
'diagnostic.lua', 'diagnostic.lua',
@ -276,50 +320,50 @@ CONFIG = {
'rpc.lua', 'rpc.lua',
'protocol.lua', 'protocol.lua',
], ],
'files': [ files=[
'runtime/lua/vim/lsp', 'runtime/lua/vim/lsp',
'runtime/lua/vim/lsp.lua', 'runtime/lua/vim/lsp.lua',
], ],
'file_patterns': '*.lua', file_patterns='*.lua',
'fn_name_prefix': '', fn_name_prefix='',
'section_name': {'lsp.lua': 'lsp'}, section_name={'lsp.lua': 'lsp'},
'section_fmt': lambda name: ( section_fmt=lambda name: (
'Lua module: vim.lsp' 'Lua module: vim.lsp'
if name.lower() == 'lsp' if name.lower() == 'lsp'
else f'Lua module: vim.lsp.{name.lower()}'), else f'Lua module: vim.lsp.{name.lower()}'),
'helptag_fmt': lambda name: ( helptag_fmt=lambda name: (
'*lsp-core*' '*lsp-core*'
if name.lower() == 'lsp' if name.lower() == 'lsp'
else f'*lsp-{name.lower()}*'), else f'*lsp-{name.lower()}*'),
'fn_helptag_fmt': lambda fstem, name, istbl: ( fn_helptag_fmt=lambda fstem, name, istbl: (
f'*vim.lsp.{name}{"" if istbl else "()"}*' if fstem == 'lsp' and name != 'client' else f'*vim.lsp.{name}{"" if istbl else "()"}*' if fstem == 'lsp' and name != 'client' else
# HACK. TODO(justinmk): class/structure support in lua2dox # HACK. TODO(justinmk): class/structure support in lua2dox
'*vim.lsp.client*' if 'lsp.client' == f'{fstem}.{name}' else '*vim.lsp.client*' if 'lsp.client' == f'{fstem}.{name}' else
f'*vim.lsp.{fstem}.{name}{"" if istbl else "()"}*'), f'*vim.lsp.{fstem}.{name}{"" if istbl else "()"}*'),
'module_override': {}, module_override={},
'append_only': [], append_only=[],
}, ),
'diagnostic': { 'diagnostic': Config(
'mode': 'lua', mode='lua',
'filename': 'diagnostic.txt', filename='diagnostic.txt',
'section_order': [ section_order=[
'diagnostic.lua', 'diagnostic.lua',
], ],
'files': ['runtime/lua/vim/diagnostic.lua'], files=['runtime/lua/vim/diagnostic.lua'],
'file_patterns': '*.lua', file_patterns='*.lua',
'fn_name_prefix': '', fn_name_prefix='',
'include_tables': False, include_tables=False,
'section_name': {'diagnostic.lua': 'diagnostic'}, section_name={'diagnostic.lua': 'diagnostic'},
'section_fmt': lambda _: 'Lua module: vim.diagnostic', section_fmt=lambda _: 'Lua module: vim.diagnostic',
'helptag_fmt': lambda _: '*diagnostic-api*', helptag_fmt=lambda _: '*diagnostic-api*',
'fn_helptag_fmt': lambda fstem, name, istbl: f'*vim.{fstem}.{name}{"" if istbl else "()"}*', fn_helptag_fmt=lambda fstem, name, istbl: f'*vim.{fstem}.{name}{"" if istbl else "()"}*',
'module_override': {}, module_override={},
'append_only': [], append_only=[],
}, ),
'treesitter': { 'treesitter': Config(
'mode': 'lua', mode='lua',
'filename': 'treesitter.txt', filename='treesitter.txt',
'section_order': [ section_order=[
'treesitter.lua', 'treesitter.lua',
'language.lua', 'language.lua',
'query.lua', 'query.lua',
@ -327,30 +371,30 @@ CONFIG = {
'languagetree.lua', 'languagetree.lua',
'dev.lua', 'dev.lua',
], ],
'files': [ files=[
'runtime/lua/vim/treesitter.lua', 'runtime/lua/vim/treesitter.lua',
'runtime/lua/vim/treesitter/', 'runtime/lua/vim/treesitter/',
], ],
'file_patterns': '*.lua', file_patterns='*.lua',
'fn_name_prefix': '', fn_name_prefix='',
'section_name': {}, section_name={},
'section_fmt': lambda name: ( section_fmt=lambda name: (
'Lua module: vim.treesitter' 'Lua module: vim.treesitter'
if name.lower() == 'treesitter' if name.lower() == 'treesitter'
else f'Lua module: vim.treesitter.{name.lower()}'), else f'Lua module: vim.treesitter.{name.lower()}'),
'helptag_fmt': lambda name: ( helptag_fmt=lambda name: (
'*lua-treesitter-core*' '*lua-treesitter-core*'
if name.lower() == 'treesitter' if name.lower() == 'treesitter'
else f'*lua-treesitter-{name.lower()}*'), else f'*lua-treesitter-{name.lower()}*'),
'fn_helptag_fmt': lambda fstem, name, istbl: ( fn_helptag_fmt=lambda fstem, name, istbl: (
f'*vim.{fstem}.{name}()*' f'*vim.{fstem}.{name}()*'
if fstem == 'treesitter' if fstem == 'treesitter'
else f'*{name}()*' else f'*{name}()*'
if name[0].isupper() if name[0].isupper()
else f'*vim.treesitter.{fstem}.{name}()*'), else f'*vim.treesitter.{fstem}.{name}()*'),
'module_override': {}, module_override={},
'append_only': [], append_only=[],
} ),
} }
param_exclude = ( param_exclude = (
@ -814,6 +858,7 @@ def para_as_map(parent, indent='', width=text_width - indentation, fmt_vimhelp=F
return chunks, xrefs return chunks, xrefs
def is_program_listing(para): def is_program_listing(para):
""" """
Return True if `para` contains a "programlisting" (i.e. a Markdown code Return True if `para` contains a "programlisting" (i.e. a Markdown code
@ -835,6 +880,7 @@ def is_program_listing(para):
return len(children) == 1 and children[0].nodeName == 'programlisting' return len(children) == 1 and children[0].nodeName == 'programlisting'
def fmt_node_as_vimhelp(parent: Element, width=text_width - indentation, indent='', def fmt_node_as_vimhelp(parent: Element, width=text_width - indentation, indent='',
fmt_vimhelp=False): fmt_vimhelp=False):
"""Renders (nested) Doxygen <para> nodes as Vim :help text. """Renders (nested) Doxygen <para> nodes as Vim :help text.
@ -910,6 +956,8 @@ def extract_from_xml(filename, target, width, fmt_vimhelp):
The `fmt_vimhelp` variable controls some special cases for use by The `fmt_vimhelp` variable controls some special cases for use by
fmt_doxygen_xml_as_vimhelp(). (TODO: ugly :) fmt_doxygen_xml_as_vimhelp(). (TODO: ugly :)
""" """
config: Config = CONFIG[target]
fns = {} # Map of func_name:docstring. fns = {} # Map of func_name:docstring.
deprecated_fns = {} # Map of func_name:docstring. deprecated_fns = {} # Map of func_name:docstring.
@ -934,7 +982,7 @@ def extract_from_xml(filename, target, width, fmt_vimhelp):
continue continue
istbl = return_type.startswith('table') # Special from lua2dox.lua. istbl = return_type.startswith('table') # Special from lua2dox.lua.
if istbl and not CONFIG[target].get('include_tables', True): if istbl and not config.include_tables:
continue continue
if return_type.startswith(('ArrayOf', 'DictionaryOf')): if return_type.startswith(('ArrayOf', 'DictionaryOf')):
@ -962,7 +1010,7 @@ def extract_from_xml(filename, target, width, fmt_vimhelp):
declname = get_child(param, 'declname') declname = get_child(param, 'declname')
if declname: if declname:
param_name = get_text(declname).strip() param_name = get_text(declname).strip()
elif CONFIG[target]['mode'] == 'lua': elif config.mode == 'lua':
# XXX: this is what lua2dox gives us... # XXX: this is what lua2dox gives us...
param_name = param_type param_name = param_type
param_type = '' param_type = ''
@ -998,11 +1046,11 @@ def extract_from_xml(filename, target, width, fmt_vimhelp):
fstem = '?' fstem = '?'
if '.' in compoundname: if '.' in compoundname:
fstem = compoundname.split('.')[0] fstem = compoundname.split('.')[0]
fstem = CONFIG[target]['module_override'].get(fstem, fstem) fstem = config.module_override.get(fstem, fstem)
vimtag = CONFIG[target]['fn_helptag_fmt'](fstem, name, istbl) vimtag = config.fn_helptag_fmt(fstem, name, istbl)
if 'fn_name_fmt' in CONFIG[target]: if config.fn_name_fmt:
name = CONFIG[target]['fn_name_fmt'](fstem, name) name = config.fn_name_fmt(fstem, name)
if istbl: if istbl:
aopen, aclose = '', '' aopen, aclose = '', ''
@ -1085,7 +1133,7 @@ def extract_from_xml(filename, target, width, fmt_vimhelp):
if 'Deprecated' in str(xrefs_all): if 'Deprecated' in str(xrefs_all):
deprecated_fns[name] = fn deprecated_fns[name] = fn
elif name.startswith(CONFIG[target]['fn_name_prefix']): elif name.startswith(config.fn_name_prefix):
fns[name] = fn fns[name] = fn
fns = collections.OrderedDict(sorted( fns = collections.OrderedDict(sorted(
@ -1102,6 +1150,8 @@ def fmt_doxygen_xml_as_vimhelp(filename, target):
1. Vim help text for functions found in `filename`. 1. Vim help text for functions found in `filename`.
2. Vim help text for deprecated functions. 2. Vim help text for deprecated functions.
""" """
config: Config = CONFIG[target]
fns_txt = {} # Map of func_name:vim-help-text. fns_txt = {} # Map of func_name:vim-help-text.
deprecated_fns_txt = {} # Map of func_name:vim-help-text. deprecated_fns_txt = {} # Map of func_name:vim-help-text.
fns, _ = extract_from_xml(filename, target, text_width, True) fns, _ = extract_from_xml(filename, target, text_width, True)
@ -1164,7 +1214,7 @@ def fmt_doxygen_xml_as_vimhelp(filename, target):
func_doc = "\n".join(map(align_tags, split_lines)) func_doc = "\n".join(map(align_tags, split_lines))
if (name.startswith(CONFIG[target]['fn_name_prefix']) if (name.startswith(config.fn_name_prefix)
and name != "nvim_error_event"): and name != "nvim_error_event"):
fns_txt[name] = func_doc fns_txt[name] = func_doc
@ -1237,9 +1287,12 @@ def main(doxygen_config, args):
for target in CONFIG: for target in CONFIG:
if args.target is not None and target != args.target: if args.target is not None and target != args.target:
continue continue
config: Config = CONFIG[target]
mpack_file = os.path.join( mpack_file = os.path.join(
base_dir, 'runtime', 'doc', base_dir, 'runtime', 'doc',
CONFIG[target]['filename'].replace('.txt', '.mpack')) config.filename.replace('.txt', '.mpack'))
if os.path.exists(mpack_file): if os.path.exists(mpack_file):
os.remove(mpack_file) os.remove(mpack_file)
@ -1255,11 +1308,10 @@ def main(doxygen_config, args):
stderr=(subprocess.STDOUT if debug else subprocess.DEVNULL)) stderr=(subprocess.STDOUT if debug else subprocess.DEVNULL))
p.communicate( p.communicate(
doxygen_config.format( doxygen_config.format(
input=' '.join( input=' '.join([f'"{file}"' for file in config.files]),
[f'"{file}"' for file in CONFIG[target]['files']]),
output=output_dir, output=output_dir,
filter=filter_cmd, filter=filter_cmd,
file_patterns=CONFIG[target]['file_patterns']) file_patterns=config.file_patterns)
.encode('utf8') .encode('utf8')
) )
if p.returncode: if p.returncode:
@ -1294,9 +1346,9 @@ def main(doxygen_config, args):
filename = os.path.basename(filename) filename = os.path.basename(filename)
name = os.path.splitext(filename)[0].lower() name = os.path.splitext(filename)[0].lower()
sectname = name.upper() if name == 'ui' else name.title() sectname = name.upper() if name == 'ui' else name.title()
sectname = CONFIG[target]['section_name'].get(filename, sectname) sectname = config.section_name.get(filename, sectname)
title = CONFIG[target]['section_fmt'](sectname) title = config.section_fmt(sectname)
section_tag = CONFIG[target]['helptag_fmt'](sectname) section_tag = config.helptag_fmt(sectname)
# Module/Section id matched against @defgroup. # Module/Section id matched against @defgroup.
# "*api-buffer*" => "api-buffer" # "*api-buffer*" => "api-buffer"
section_id = section_tag.strip('*') section_id = section_tag.strip('*')
@ -1319,22 +1371,22 @@ def main(doxygen_config, args):
if len(sections) == 0: if len(sections) == 0:
fail(f'no sections for target: {target} (look for errors near "Preprocessing" log lines above)') fail(f'no sections for target: {target} (look for errors near "Preprocessing" log lines above)')
if len(sections) > len(CONFIG[target]['section_order']): if len(sections) > len(config.section_order):
raise RuntimeError( raise RuntimeError(
'found new modules "{}"; update the "section_order" map'.format( 'found new modules "{}"; update the "section_order" map'.format(
set(sections).difference(CONFIG[target]['section_order']))) set(sections).difference(config.section_order)))
first_section_tag = sections[CONFIG[target]['section_order'][0]][1] first_section_tag = sections[config.section_order[0]][1]
docs = '' docs = ''
for filename in CONFIG[target]['section_order']: for filename in config.section_order:
try: try:
title, section_tag, section_doc = sections.pop(filename) title, section_tag, section_doc = sections.pop(filename)
except KeyError: except KeyError:
msg(f'warning: empty docs, skipping (target={target}): {filename}') msg(f'warning: empty docs, skipping (target={target}): {filename}')
msg(f' existing docs: {sections.keys()}') msg(f' existing docs: {sections.keys()}')
continue continue
if filename not in CONFIG[target]['append_only']: if filename not in config.append_only:
docs += sep docs += sep
docs += '\n{}{}'.format(title, section_tag.rjust(text_width - len(title))) docs += '\n{}{}'.format(title, section_tag.rjust(text_width - len(title)))
docs += section_doc docs += section_doc
@ -1343,8 +1395,7 @@ def main(doxygen_config, args):
docs = docs.rstrip() + '\n\n' docs = docs.rstrip() + '\n\n'
docs += f' vim:tw=78:ts=8:sw={indentation}:sts={indentation}:et:ft=help:norl:\n' docs += f' vim:tw=78:ts=8:sw={indentation}:sts={indentation}:et:ft=help:norl:\n'
doc_file = os.path.join(base_dir, 'runtime', 'doc', doc_file = os.path.join(base_dir, 'runtime', 'doc', config.filename)
CONFIG[target]['filename'])
if os.path.exists(doc_file): if os.path.exists(doc_file):
delete_lines_below(doc_file, first_section_tag) delete_lines_below(doc_file, first_section_tag)