From c031e7b4322345f098fe8fc380eae61664d56d52 Mon Sep 17 00:00:00 2001 From: Armando Montanez Date: Thu, 30 Apr 2026 09:48:17 -0700 Subject: [PATCH] docs: Special-case command line reference conversion Special-cases the command line reference conversion so the generated MDX is formatted in a usable way. Since the HTML is rather rigid and predictable, this is sufficient for the initial migration away from DevSite. Eventually, the command line reference should be generated externally from e.g. proto so the generation mechanism is not tied to the bazel version. --- scripts/docs/BUILD | 15 + scripts/docs/clr_converter.py | 691 ++++++++++++++++++ scripts/docs/clr_converter_test.py | 229 ++++++ scripts/docs/docs2mdx.py | 9 +- scripts/docs/testdata/clr_expected.mdx | 89 +++ scripts/docs/testdata/clr_input.html | 115 +++ .../bazel/repository/RepositoryOptions.java | 2 +- 7 files changed, 1148 insertions(+), 2 deletions(-) create mode 100644 scripts/docs/clr_converter.py create mode 100644 scripts/docs/clr_converter_test.py create mode 100644 scripts/docs/testdata/clr_expected.mdx create mode 100644 scripts/docs/testdata/clr_input.html diff --git a/scripts/docs/BUILD b/scripts/docs/BUILD index bbb85b99a079eb..4db9bb02125da5 100644 --- a/scripts/docs/BUILD +++ b/scripts/docs/BUILD @@ -97,6 +97,20 @@ py_test( ], ) +py_library( + name = "clr_converter", + srcs = ["clr_converter.py"], +) + +py_test( + name = "clr_converter_test", + srcs = ["clr_converter_test.py"], + data = [":testdata"], + deps = [ + ":clr_converter", + ], +) + py_binary( name = "docs2mdx", srcs = ["docs2mdx.py"], @@ -104,6 +118,7 @@ py_binary( "//src/main/java/com/google/devtools/build/lib:__pkg__", ], deps = [ + ":clr_converter", "//third_party/py/abseil", requirement("markdownify"), ], diff --git a/scripts/docs/clr_converter.py b/scripts/docs/clr_converter.py new file mode 100644 index 00000000000000..550fe0c359625c --- /dev/null +++ b/scripts/docs/clr_converter.py @@ -0,0 +1,691 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Converts command-line-reference HTML to MDX with ParamField components.""" + +import dataclasses +import html +import html.parser +import re + + +@dataclasses.dataclass +class _Option: + anchor_id: str = "" + flag_name: str = "" + value_type: str = "" + abbreviation: str = "" + default: str = "" + allow_multiple: bool = False + is_deprecated: bool = False + help_html: str = "" + expansion_flags: list = dataclasses.field(default_factory=list) + tags_html: str = "" + + +@dataclasses.dataclass +class _Section: + heading: str = "" + anchor: str = "" + inherits_html: str = "" + categories: list = dataclasses.field(default_factory=list) + + +@dataclasses.dataclass +class _Category: + description: str = "" + options: list = dataclasses.field(default_factory=list) + + +@dataclasses.dataclass +class _TagTable: + heading: str = "" + rows: list = dataclasses.field(default_factory=list) + + +_REPLACED_JS_CHARACTERS = { + "{": "{", + "}": "}", +} + +_REPLACED_CODE_CHARACTERS = { + "<": "<", + ">": ">", + **_REPLACED_JS_CHARACTERS, +} + + +def _escape_mdx(text): + for c, replacement in _REPLACED_JS_CHARACTERS.items(): + text = text.replace(c, replacement) + return text + + +def _escape_mdx_code(text): + # Escape $ so MDX doesn't parse it as a LaTeX math delimiter or, + # combined with {, as a JS template expression. + text = text.replace("$", "$") + for c, replacement in _REPLACED_CODE_CHARACTERS.items(): + text = text.replace(c, replacement) + return text + + +class _CLRParser(html.parser.HTMLParser): + """Parses the command-line-reference HTML into structured data.""" + + def __init__(self): + super().__init__() + self.prefix_html = "" + self.commands_table_rows = [] + self.sections = [] + self.tag_tables = [] + + self._state = "PREFIX" + self._depth = 0 + self._tag_stack = [] + + self._current_section = None + self._current_category = None + self._current_option = None + self._current_tag_table = None + + self._in_dt = False + self._in_dd = False + self._in_h2 = False + self._in_h3 = False + self._in_commands_table = False + self._in_tag_table = False + self._in_table_row = False + self._table_cells = [] + self._current_cell_html = "" + self._in_table_cell = False + self._in_dl = False + self._dl_text_buffer = "" + self._inherits_buffer = "" + + self._dt_html = "" + self._dd_html = "" + self._heading_text = "" + self._heading_anchor = "" + + self._prefix_parts = [] + self._saw_commands_heading = False + + def handle_starttag(self, tag, attrs): + attrs_dict = dict(attrs) + + if self._state == "PREFIX": + if tag == "h2": + self._in_h2 = True + self._heading_text = "" + return + self._prefix_parts.append(self._rebuild_tag(tag, attrs)) + return + + if self._in_dt: + self._dt_html += self._rebuild_tag(tag, attrs) + return + + if self._in_dd: + self._dd_html += self._rebuild_tag(tag, attrs) + return + + if self._in_table_cell: + self._current_cell_html += self._rebuild_tag(tag, attrs) + return + + if tag == "h2": + self._in_h2 = True + self._heading_text = "" + self._heading_anchor = "" + return + + if tag == "h3": + self._in_h3 = True + self._heading_text = "" + return + + if tag == "a" and self._in_h2: + name = attrs_dict.get("name", "") + if name: + self._heading_anchor = name + return + + if tag == "table": + if self._in_h3 or (self._current_tag_table is not None): + self._in_tag_table = True + elif self._state == "COMMANDS_TABLE": + self._in_commands_table = True + return + + if tag == "tr" and (self._in_commands_table or self._in_tag_table): + self._in_table_row = True + self._table_cells = [] + return + + if tag == "td" and self._in_table_row: + self._in_table_cell = True + self._current_cell_html = "" + cell_id = attrs_dict.get("id", "") + if cell_id: + self._current_cell_html += f'' + return + + if tag == "p" and self._current_section and not self._in_dl: + self._state = "INHERITS_P" + self._inherits_buffer = "" + return + + if tag == "a" and self._state == "INHERITS_P": + self._inherits_buffer += self._rebuild_tag(tag, attrs) + return + + if tag == "dl": + self._in_dl = True + self._dl_text_buffer = "" + return + + if tag == "dt": + self._in_dt = True + self._dt_html = "" + anchor_id = attrs_dict.get("id", "") + self._current_option = _Option(anchor_id=anchor_id) + return + + if tag == "dd": + self._in_dd = True + self._dd_html = "" + return + + if tag == "p" and self._in_dl and not self._in_dt and not self._in_dd: + self._in_dd = True + self._dd_html = "

" + return + + def handle_endtag(self, tag): + if self._state == "PREFIX": + if tag == "h2" and self._in_h2: + self._in_h2 = False + if self._heading_text.strip() == "Commands": + self.prefix_html = "".join(self._prefix_parts) + self._state = "COMMANDS_TABLE" + else: + self._prefix_parts.append(f"

{self._heading_text}

") + return + self._prefix_parts.append(f"") + return + + if self._in_dt: + if tag == "dt": + self._in_dt = False + self._parse_dt(self._dt_html) + return + self._dt_html += f"" + return + + if self._in_dd: + if tag == "dd": + self._in_dd = False + self._parse_dd(self._dd_html) + if self._current_option and self._current_category: + self._current_category.options.append(self._current_option) + self._current_option = None + return + self._dd_html += f"" + return + + if self._in_table_cell: + if tag == "td": + self._in_table_cell = False + self._table_cells.append(self._current_cell_html) + return + self._current_cell_html += f"" + return + + if tag == "tr" and self._in_table_row: + self._in_table_row = False + if self._in_commands_table: + self.commands_table_rows.append(self._table_cells) + elif self._in_tag_table and self._current_tag_table: + self._current_tag_table.rows.append(self._table_cells) + return + + if tag == "a" and self._state == "INHERITS_P": + self._inherits_buffer += "" + return + + if tag == "p" and self._state == "INHERITS_P": + if self._current_section: + self._current_section.inherits_html = self._inherits_buffer + self._state = "BODY" + return + + if tag == "table": + if self._in_commands_table: + self._in_commands_table = False + self._state = "BODY" + elif self._in_tag_table: + self._in_tag_table = False + if self._current_tag_table: + self.tag_tables.append(self._current_tag_table) + self._current_tag_table = None + return + + if tag == "h2" and self._in_h2: + self._in_h2 = False + self._finish_current_section() + self._current_section = _Section( + heading=self._heading_text.strip(), + anchor=self._heading_anchor, + ) + return + + if tag == "h3" and self._in_h3: + self._in_h3 = False + heading = self._heading_text.strip() + if "Option" in heading and "Tag" in heading: + self._current_tag_table = _TagTable(heading=heading) + return + + if tag == "dl": + self._in_dl = False + if self._current_category: + if self._current_section: + self._current_section.categories.append(self._current_category) + self._current_category = None + return + + if tag == "p" and self._in_dl and not self._in_dt and not self._in_dd: + pass + + def handle_data(self, data): + if self._state == "PREFIX": + if self._in_h2: + self._heading_text += data + else: + self._prefix_parts.append(html.escape(data).replace("'", "'")) + return + + if self._in_h2 or self._in_h3: + self._heading_text += data + return + + if self._in_dt: + self._dt_html += html.escape(data) + return + + if self._in_dd: + self._dd_html += html.escape(data) + return + + if self._in_table_cell: + self._current_cell_html += html.escape(data) + return + + if self._in_dl and not self._in_dt and not self._in_dd: + stripped = data.strip().rstrip(":") + if stripped: + self._current_category = _Category(description=html.unescape(stripped)) + return + + if self._state == "INHERITS_P": + self._inherits_buffer += html.escape(data) + return + + def handle_entityref(self, name): + text = f"&{name};" + if self._in_dt: + self._dt_html += text + elif self._in_dd: + self._dd_html += text + elif self._in_table_cell: + self._current_cell_html += text + elif self._state == "INHERITS_P": + self._inherits_buffer += text + elif self._state == "PREFIX" and not self._in_h2: + self._prefix_parts.append(text) + + def handle_charref(self, name): + text = f"&#{name};" + if self._in_dt: + self._dt_html += text + elif self._in_dd: + self._dd_html += text + elif self._in_table_cell: + self._current_cell_html += text + + def close(self): + super().close() + self._finish_current_section() + + def _finish_current_section(self): + if self._current_category and self._current_section: + self._current_section.categories.append(self._current_category) + self._current_category = None + if self._current_section: + self.sections.append(self._current_section) + self._current_section = None + + def _rebuild_tag(self, tag, attrs): + parts = [tag] + for k, v in attrs: + if v is None: + parts.append(k) + else: + parts.append(f'{k}="{v}"') + return "<" + " ".join(parts) + ">" + + def _parse_dt(self, dt_html): + opt = self._current_option + if not opt: + return + + flag_match = re.search(r']*>([^<]+)', dt_html) + if flag_match: + opt.flag_name = flag_match.group(1) + + code_match = re.search(r']*>(.*?)', dt_html, re.DOTALL) + if code_match: + code_content = code_match.group(1) + after_a = re.split(r'', code_content, maxsplit=1) + if len(after_a) > 1: + value_part = after_a[1].strip() + if value_part.startswith("="): + raw_type = html.unescape(value_part[1:]).strip() + if raw_type.startswith("<") and raw_type.endswith(">"): + raw_type = raw_type[1:-1] + opt.value_type = raw_type + + after_code = re.split(r'\s*', dt_html, maxsplit=1) + if len(after_code) > 1: + remainder = after_code[1] + + abbrev_match = re.search(r'\[(-\w)\]', remainder) + if abbrev_match: + opt.abbreviation = abbrev_match.group(1) + remainder = remainder[abbrev_match.end():] + + remainder = remainder.strip() + if "multiple uses are accumulated" in remainder: + opt.allow_multiple = True + elif remainder.startswith("default:"): + default_val = remainder[len("default:"):].strip() + default_val = html.unescape(default_val).strip('"') + opt.default = default_val + + if not opt.value_type and not opt.flag_name.startswith("--[no]"): + opt.value_type = "void" + elif not opt.value_type and opt.flag_name.startswith("--[no]"): + opt.value_type = "boolean" + + def _parse_dd(self, dd_html): + opt = self._current_option + if not opt: + return + + parts = re.split(r'

', dd_html) + help_parts = [] + for part in parts: + part_text = part.strip() + if not part_text: + continue + if part_text.startswith("Expands to:"): + exp_flags = re.findall( + r']*>([^<]+)', part_text) + opt.expansion_flags = exp_flags + elif part_text.startswith("Tags:"): + opt.tags_html = "

" + part_text + if "deprecated" in part_text.lower(): + if "metadata_tag_DEPRECATED" in part_text: + opt.is_deprecated = True + else: + help_parts.append(part_text.removesuffix("

")) + + opt.help_html = "\n".join(help_parts) + + +def _escape_mdx_outside_code(text): + """Escapes MDX-special characters outside of code spans and fenced blocks.""" + # First split on fenced code blocks (``` ... ```), then within non-fenced + # segments split on inline backtick spans. + fenced_parts = re.split(r'(```[^`]*```)', text, flags=re.DOTALL) + result = [] + for fi, fenced_part in enumerate(fenced_parts): + if fi % 2 == 1: + result.append(fenced_part) + continue + inline_parts = re.split(r'(`[^`]*`)', fenced_part) + for ii, inline_part in enumerate(inline_parts): + if ii % 2 == 0: + inline_part = _escape_mdx_code(inline_part) + result.append(inline_part) + return "".join(result) + + +_PRE_CODE_RE = re.compile( + r'
(.*?)
', re.DOTALL) + + +def _convert_pre_code_block(m): + content = html.unescape(m.group(1)).strip('\n') + return f'\n\n```\n{content}\n```\n\n' + + +_ERRATA = [ + ("`rewrite, allow, block'", "`rewrite, allow, block`"), +] + + +def _apply_known_errata(text): + for old, new in _ERRATA: + text = text.replace(old, new) + return text + + +def _html_to_simple_md(html_content): + """Minimal HTML-to-markdown for option help text.""" + text = html_content + + # Fenced code blocks before inline code so
 isn't consumed
+  # by the inline pattern. Content is unescaped but not MDX-escaped since
+  # fenced blocks are literal in MDX.
+  text = _PRE_CODE_RE.sub(_convert_pre_code_block, text)
+
+  text = re.sub(r']*>([^<]*)', r'[\2](\1)', text)
+  text = re.sub(r'([^<]*)', lambda m: '`' + m.group(1) + '`', text)
+  text = re.sub(r'([^<]*)', r'*\1*', text)
+  text = re.sub(r'([^<]*)', r'**\1**', text)
+  text = re.sub(r'', '\n', text)
+
+  text = re.sub(r'
  • \s*', '- ', text) + text = re.sub(r'
  • ', '', text) + text = re.sub(r'', '', text) + + text = re.sub(r'

    ', '\n\n', text) + text = re.sub(r'

    ', '', text) + + text = re.sub(r'<[^>]+>', '', text) + + text = html.unescape(text) + text = _apply_known_errata(text) + text = _escape_mdx_outside_code(text) + text = re.sub(r'\n{3,}', '\n\n', text) + return text.strip() + + +def _escape_attr(value): + """Escapes a string for use inside a JSX attribute value.""" + escaped = value.replace("&", "&") + escaped = escaped.replace('"', """) + escaped = escaped.replace("<", "<") + escaped = escaped.replace(">", ">") + escaped = escaped.replace("{", "{") + escaped = escaped.replace("}", "}") + return escaped + + +def _render_option(opt): + """Renders a single option as a ParamField component.""" + attrs = [f'path="{_escape_attr(opt.flag_name)}"'] + + if opt.value_type: + attrs.append(f'type="{_escape_attr(opt.value_type)}"') + + if opt.default and not opt.allow_multiple: + attrs.append(f'default="{_escape_attr(opt.default)}"') + + if opt.is_deprecated: + attrs.append("deprecated") + + attr_str = " ".join(attrs) + lines = [f""] + + if opt.abbreviation: + lines.append(f" Short form: `{opt.abbreviation}`") + lines.append("") + + help_md = _html_to_simple_md(opt.help_html) + if help_md: + for line in help_md.split("\n"): + lines.append(f" {line}" if line.strip() else "") + + if opt.allow_multiple: + lines.append("") + lines.append(" *May be used multiple times; values are accumulated.*") + + if opt.expansion_flags: + lines.append("") + lines.append(" Expands to:") + for flag in opt.expansion_flags: + lines.append(f" - `{_escape_mdx(flag)}`") + + if opt.tags_html: + tags_md = _render_tags(opt.tags_html) + if tags_md: + lines.append("") + lines.append(f" {tags_md}") + + lines.append("") + return "\n".join(lines) + + +def _render_tags(tags_html): + """Converts tags HTML to markdown.""" + tag_links = re.findall( + r'([^<]*)', tags_html) + if not tag_links: + return "" + parts = [f"[`{name}`]({href})" for href, name in tag_links] + return "Tags: " + ", ".join(parts) + + +def _render_commands_table(rows): + """Renders the commands table as markdown.""" + lines = [ + "## Commands", + "", + "| | |", + "| --- | --- |", + ] + for cells in rows: + if len(cells) >= 2: + cmd_md = re.sub( + r'([^<]*)', + lambda m: f"[`{m.group(2)}`]({m.group(1)})", + cells[0], + ) + desc = html.unescape(re.sub(r'<[^>]+>', '', cells[1])) + lines.append(f"| {cmd_md} | {_escape_mdx(desc)} |") + return "\n".join(lines) + + +def _render_tag_table(tag_table): + """Renders a tag description table as markdown.""" + lines = [ + f"### {tag_table.heading}", + "", + "| | |", + "| --- | --- |", + ] + for cells in tag_table.rows: + if len(cells) >= 2: + name_cell = cells[0] + span_match = re.search(r'', name_cell) + code_match = re.search(r'([^<]*)', name_cell) + if span_match and code_match: + tag_id = span_match.group(1) + tag_name = code_match.group(1) + name_md = f'`{tag_name}`' + else: + name_md = re.sub(r'<[^>]+>', '', name_cell) + desc = html.unescape(re.sub(r'<[^>]+>', '', cells[1])) + lines.append(f"| {name_md} | {_escape_mdx(desc)} |") + return "\n".join(lines) + + +def _render_section(section): + """Renders a section heading, inherits info, and all options.""" + lines = [f"## {section.heading}"] + if section.anchor: + lines[0] = f'## {section.heading} {{#{section.anchor}}}' + + if section.inherits_html: + inherits_md = re.sub( + r'([^<]*)', + lambda m: f"[{m.group(2)}]({m.group(1)})", + section.inherits_html, + ) + inherits_md = re.sub(r'<[^>]+>', '', inherits_md).strip() + if inherits_md: + lines.append("") + lines.append(inherits_md) + + for cat in section.categories: + lines.append("") + if cat.description: + lines.append(cat.description) + lines.append("") + for opt in cat.options: + lines.append(_render_option(opt)) + lines.append("") + + return "\n".join(lines) + + +def convert(html_content): + """Converts command-line-reference HTML to MDX with ParamField components. + + Args: + html_content: str; the full HTML content of command-line-reference.html. + + Returns: + The MDX content with ParamField components. + """ + parser = _CLRParser() + parser.feed(html_content) + parser.close() + + parts = [] + + parts.append(_render_commands_table(parser.commands_table_rows)) + parts.append("") + + for section in parser.sections: + parts.append(_render_section(section)) + parts.append("") + + for tag_table in parser.tag_tables: + parts.append(_render_tag_table(tag_table)) + parts.append("") + + return "\n".join(parts) diff --git a/scripts/docs/clr_converter_test.py b/scripts/docs/clr_converter_test.py new file mode 100644 index 00000000000000..b56e73633817ac --- /dev/null +++ b/scripts/docs/clr_converter_test.py @@ -0,0 +1,229 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import unittest +from scripts.docs import clr_converter + + +def _read_data_file(basename): + path = os.path.join( + os.getenv("TEST_SRCDIR"), + os.getenv("TEST_WORKSPACE"), + "scripts/docs/testdata", + basename) + with open(path, "rt", encoding="utf-8") as f: + return f.read() + + +class ConvertTest(unittest.TestCase): + + def test_full_conversion(self): + html_content = _read_data_file("clr_input.html") + expected = _read_data_file("clr_expected.mdx") + actual = clr_converter.convert(html_content) + self.assertEqual(actual, expected) + + def test_boolean_option(self): + html_input = """ +

    Commands

    +
    +

    Options

    +
    Category: +
    --[no]batch default: "false"
    +

    Run in batch mode.

    +
    + """ + result = clr_converter.convert(html_input) + self.assertIn('path="--[no]batch"', result) + self.assertIn('type="boolean"', result) + self.assertIn('default="false"', result) + + def test_typed_option(self): + html_input = """ +

    Commands

    +
    +

    Options

    +
    Category: +
    --jobs=<an integer> default: "auto"
    +

    Number of jobs.

    +
    + """ + result = clr_converter.convert(html_input) + self.assertIn('path="--jobs"', result) + self.assertIn('type="an integer"', result) + self.assertIn('default="auto"', result) + + def test_void_expansion_option(self): + html_input = """ +

    Commands

    +
    +

    Options

    +
    Category: +
    --debug
    +
    +

    Enable debug mode.

    +

    Expands to: +
      --verbose +

    +
    + """ + result = clr_converter.convert(html_input) + self.assertIn('type="void"', result) + self.assertIn("Expands to:", result) + self.assertIn("`--verbose`", result) + self.assertNotIn("default=", result) + + def test_allow_multiple_option(self): + html_input = """ +

    Commands

    +
    +

    Options

    +
    Category: +
    --rc=<path> multiple uses are accumulated
    +

    RC file location.

    +
    + """ + result = clr_converter.convert(html_input) + self.assertIn("May be used multiple times", result) + self.assertNotIn("default=", result) + + def test_deprecated_option(self): + html_input = """ +

    Commands

    +
    +

    Options

    +
    Category: +
    --[no]old default: "false"
    +

    Old flag.

    +

    Tags: + deprecated +

    +
    + """ + result = clr_converter.convert(html_input) + self.assertIn("deprecated", result) + + def test_abbreviation(self): + html_input = """ +

    Commands

    +
    +

    Options

    +
    Category: +
    --jobs=<an integer> [-j] default: "8"
    +

    Number of jobs.

    +
    + """ + result = clr_converter.convert(html_input) + self.assertIn("Short form: `-j`", result) + + def test_inherits_from(self): + html_input = """ +

    Commands

    +
    +

    Build Options

    +

    Inherits all options from common and test.

    +
    Cat: +
    --[no]f default: "true"
    +

    A flag.

    +
    + """ + result = clr_converter.convert(html_input) + self.assertIn("Inherits all options from [common](#common) and [test](#test)", result) + self.assertIn("{#build}", result) + + def test_enum_type_escaping(self): + html_input = """ +

    Commands

    +
    +

    Options

    +
    Category: +
    --level={0,1,2} default: "0"
    +

    Set level.

    +
    + """ + result = clr_converter.convert(html_input) + self.assertIn("{0,1,2}", result) + + def test_quotes_in_type_description(self): + html_input = """ +

    Commands

    +
    +

    Options

    +
    Category: +
    --jobs=<an integer, or a keyword ("auto", "HOST_CPUS")> default: "auto"
    +

    Number of jobs.

    +
    + """ + result = clr_converter.convert(html_input) + self.assertNotIn('("auto"', result) + self.assertIn(""auto"", result) + + def test_angle_brackets_in_type_description(self): + html_input = """ +

    Commands

    +
    +

    Options

    +
    Category: +
    --mem=<an integer or "HOST_RAM", followed by [-|*]<float>> default: "0"
    +

    Memory limit.

    +
    + """ + result = clr_converter.convert(html_input) + for line in result.split("\n"): + if "Commands + + + + + +
    buildBuild targets.
    +

    Options

    +
    Cat: +
    --[no]f default: "true"
    +

    Flag.

    +
    + """ + result = clr_converter.convert(html_input) + self.assertIn("## Commands", result) + self.assertIn("[`build`](#build)", result) + self.assertIn("Build targets.", result) + + def test_tag_tables(self): + html_input = """ +

    Commands

    +
    +

    Option Effect Tags

    + + + + + +
    fooFoo description.
    + """ + result = clr_converter.convert(html_input) + self.assertIn("### Option Effect Tags", result) + self.assertIn('`foo`', result) + self.assertIn("Foo description.", result) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/docs/docs2mdx.py b/scripts/docs/docs2mdx.py index 133c1cf92ead80..30f5d2b908e685 100644 --- a/scripts/docs/docs2mdx.py +++ b/scripts/docs/docs2mdx.py @@ -22,6 +22,7 @@ from absl import app from absl import flags import markdownify +from scripts.docs import clr_converter FLAGS = flags.FLAGS @@ -156,7 +157,13 @@ def _convert_file(src, dest): def _transform(path, content): content = _pre_markdown_transforms(content) - md = _html2md(content) if path.endswith(".html") else content + if path.endswith(".html"): + if os.path.basename(path) == "command-line-reference.html": + md = clr_converter.convert(content) + else: + md = _html2md(content) + else: + md = content return _post_markdown_transforms(md) diff --git a/scripts/docs/testdata/clr_expected.mdx b/scripts/docs/testdata/clr_expected.mdx new file mode 100644 index 00000000000000..180587a82f1302 --- /dev/null +++ b/scripts/docs/testdata/clr_expected.mdx @@ -0,0 +1,89 @@ +## Commands + +| | | +| --- | --- | +| [`build`](#build) | Builds the specified targets. | +| [`test`](#test) | Builds and runs the specified test targets. | + +## Startup Options + +Options that appear before the command and are parsed by the client + + + If set, Bazel will be run as just a client process without a server. + + Tags: [`loses_incremental_state`](#effect_tag_LOSES_INCREMENTAL_STATE), [`deprecated`](#metadata_tag_DEPRECATED) + + + + The amount of time the client waits for each attempt to connect to the server + + Tags: [`bazel_internal_configuration`](#effect_tag_BAZEL_INTERNAL_CONFIGURATION) + + + + The location of the user .bazelrc file. + + *May be used multiple times; values are accumulated.* + + Tags: [`changes_inputs`](#effect_tag_CHANGES_INPUTS) + + + + Convenience option to add some additional JVM startup flags. + + Expands to: + - `--host_jvm_args=-agentlib:jdwp=transport=dt_socket,server=y,address=5005` + + + + The hash function to use when computing file digests. + + Tags: [`loses_incremental_state`](#effect_tag_LOSES_INCREMENTAL_STATE) + + + + Only on Linux; set a level from 0-7 for best-effort IO scheduling. + + Tags: [`host_machine_resource_optimizations`](#effect_tag_HOST_MACHINE_RESOURCE_OPTIMIZATIONS) + + + +## Options Common to all Commands {#common_options} + +Verbosity options + + + If true, enable verbose output. + + Tags: [`affects_outputs`](#effect_tag_AFFECTS_OUTPUTS) + + + +## Build Options {#build} + +Inherits all options from [common_options](#common_options). + +Build-specific options + + + Short form: `-j` + + The number of concurrent jobs to run. Takes an integer, or "auto". + + Tags: [`execution`](#effect_tag_EXECUTION) + + + +### Option Effect Tags + +| | | +| --- | --- | +| `loses_incremental_state` | This option affects the incremental state. | +| `affects_outputs` | This option affects the outputs. | + +### Option Metadata Tags + +| | | +| --- | --- | +| `deprecated` | This option is deprecated. | diff --git a/scripts/docs/testdata/clr_input.html b/scripts/docs/testdata/clr_input.html new file mode 100644 index 00000000000000..caf284b34947d0 --- /dev/null +++ b/scripts/docs/testdata/clr_input.html @@ -0,0 +1,115 @@ + + + + + + + +

    Command-Line Reference

    + +
    +bazel [<startup options>] <command> [<args>]
    +
    + +

    Option Syntax

    + +

    +Options can be passed to Bazel in different ways. +

    + +

    Commands

    + + + + + + + + + +
    buildBuilds the specified targets.
    testBuilds and runs the specified test targets.
    + +

    Startup Options

    +
    Options that appear before the command and are parsed by the client: +
    --[no]batch default: "false"
    +
    +

    If set, Bazel will be run as just a client process without a server.

    +

    Tags: +loses_incremental_state, deprecated +

    +
    --connect_timeout_secs=<an integer> default: "30"
    +
    +

    The amount of time the client waits for each attempt to connect to the server

    +

    Tags: +bazel_internal_configuration +

    +
    --bazelrc=<path> multiple uses are accumulated
    +
    +

    The location of the user .bazelrc file.

    +

    Tags: +changes_inputs +

    +
    --host_jvm_debug
    +
    +

    Convenience option to add some additional JVM startup flags.

    +

    Expands to: +
      --host_jvm_args=-agentlib:jdwp=transport=dt_socket,server=y,address=5005 +

    +
    --digest_function=<hash function> default: see description
    +
    +

    The hash function to use when computing file digests.

    +

    Tags: +loses_incremental_state +

    +
    --io_nice_level={-1,0,1,2,3,4,5,6,7} default: "-1"
    +
    +

    Only on Linux; set a level from 0-7 for best-effort IO scheduling.

    +

    Tags: +host_machine_resource_optimizations +

    +
    + +

    Options Common to all Commands

    +
    Verbosity options: +
    --[no]verbose default: "false"
    +
    +

    If true, enable verbose output.

    +

    Tags: +affects_outputs +

    +
    + +

    Build Options

    +

    Inherits all options from common_options.

    + +
    Build-specific options: +
    --jobs=<an integer> [-j] default: "auto"
    +
    +

    The number of concurrent jobs to run. Takes an integer, or "auto".

    +

    Tags: +execution +

    +
    + +

    Option Effect Tags

    + + + + + + + + + +
    loses_incremental_stateThis option affects the incremental state.
    affects_outputsThis option affects the outputs.
    + +

    Option Metadata Tags

    + + + + + +
    deprecatedThis option is deprecated.
    + + + diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java index cb7266b4ff9c69..49aea7cdce4139 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java @@ -291,7 +291,7 @@ verified by hashes stored in the registry (and thus pinned by the lockfile). This file consists of directives, one per line, that adjust how the Bazel downloader acts. The directives are: `allow`, `block`, `rewrite`, and `all_blocked_message`. The directives are applied in the order - `rewrite, allow, block'. + `rewrite, allow, block`. Comments are allowed and must be on their own line (no trailing comments) and preceded by a `#`. Example: `# evil.com is known to host malicious code`