esp-docs 2.1.5__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. {esp_docs-2.1.5 → esp_docs-2.2.0}/PKG-INFO +2 -2
  2. esp_docs-2.2.0/pyproject.toml +16 -0
  3. {esp_docs-2.1.5 → esp_docs-2.2.0}/setup.cfg +1 -1
  4. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/build_docs.py +27 -0
  5. esp_docs-2.2.0/src/esp_docs/check_lang_switch.py +517 -0
  6. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs.egg-info/PKG-INFO +2 -2
  7. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs.egg-info/SOURCES.txt +1 -0
  8. esp_docs-2.1.5/pyproject.toml +0 -6
  9. {esp_docs-2.1.5 → esp_docs-2.2.0}/README.md +0 -0
  10. {esp_docs-2.1.5 → esp_docs-2.2.0}/setup.py +0 -0
  11. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/__init__.py +0 -0
  12. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/_static/DejaVuSans.ttf +0 -0
  13. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/_static/NotoSansSC-Regular.otf +0 -0
  14. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/_static/espressif-logo.svg +0 -0
  15. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/_static/espressif2.pdf +0 -0
  16. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/_static/theme_overrides.css +0 -0
  17. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/check_docs.py +0 -0
  18. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/conf_docs.py +0 -0
  19. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/constants.py +0 -0
  20. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/deploy_docs.py +0 -0
  21. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/deploy_docs_s3.py +0 -0
  22. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/esp_extensions/__init__.py +0 -0
  23. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/esp_extensions/add_html_zip.py +0 -0
  24. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/esp_extensions/dummy_build_system/__init__.py +0 -0
  25. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/esp_extensions/exclude_docs.py +0 -0
  26. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/esp_extensions/format_esp_target.py +0 -0
  27. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/esp_extensions/include_build_file.py +0 -0
  28. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/esp_extensions/latex_builder.py +0 -0
  29. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/esp_extensions/link_roles.py +0 -0
  30. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/esp_extensions/run_doxygen.py +0 -0
  31. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/fonts/DejaVuSans.ttf +0 -0
  32. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/fonts/NotoSansSC-Regular.otf +0 -0
  33. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/generic_extensions/__init__.py +0 -0
  34. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/generic_extensions/add_warnings.py +0 -0
  35. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/generic_extensions/google_analytics.py +0 -0
  36. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/generic_extensions/html_redirects.py +0 -0
  37. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/generic_extensions/list_filter.py +0 -0
  38. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/generic_extensions/toctree_filter.py +0 -0
  39. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/get_github_rev.py +0 -0
  40. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/idf_extensions/__init__.py +0 -0
  41. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/idf_extensions/build_system/CMakeLists.txt +0 -0
  42. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/idf_extensions/build_system/__init__.py +0 -0
  43. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/idf_extensions/esp_err_definitions.py +0 -0
  44. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/idf_extensions/gen_defines.py +0 -0
  45. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/idf_extensions/gen_idf_tools_links.py +0 -0
  46. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/idf_extensions/gen_toolchain_links.py +0 -0
  47. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/idf_extensions/gen_version_specific_includes.py +0 -0
  48. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/idf_extensions/kconfig_reference.py +0 -0
  49. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/latex_templates/espidf.sty +0 -0
  50. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/latex_templates/preamble.tex +0 -0
  51. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/latex_templates/titlepage.tex +0 -0
  52. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/sanitize_version.py +0 -0
  53. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/upload_docs_s3.py +0 -0
  54. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/util/__init__.py +0 -0
  55. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/util/util.py +0 -0
  56. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/__init__.py +0 -0
  57. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/builder.py +0 -0
  58. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/command.py +0 -0
  59. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/drawer.py +0 -0
  60. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/elements.py +0 -0
  61. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/imagedraw/__init__.py +0 -0
  62. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/imagedraw/base.py +0 -0
  63. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/imagedraw/filters/__init__.py +0 -0
  64. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/imagedraw/filters/linejump.py +0 -0
  65. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/imagedraw/pdf.py +0 -0
  66. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/imagedraw/png.py +0 -0
  67. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/imagedraw/simplesvg.py +0 -0
  68. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/imagedraw/svg.py +0 -0
  69. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/imagedraw/textfolder.py +0 -0
  70. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/imagedraw/utils/__init__.py +0 -0
  71. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/imagedraw/utils/ellipse.py +0 -0
  72. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/metrics.py +0 -0
  73. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/__init__.py +0 -0
  74. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/actor.py +0 -0
  75. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/base.py +0 -0
  76. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/beginpoint.py +0 -0
  77. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/box.py +0 -0
  78. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/circle.py +0 -0
  79. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/cloud.py +0 -0
  80. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/diamond.py +0 -0
  81. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/dots.py +0 -0
  82. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/ellipse.py +0 -0
  83. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/endpoint.py +0 -0
  84. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/flowchart/__init__.py +0 -0
  85. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/flowchart/database.py +0 -0
  86. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/flowchart/input.py +0 -0
  87. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/flowchart/loopin.py +0 -0
  88. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/flowchart/loopout.py +0 -0
  89. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/flowchart/terminator.py +0 -0
  90. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/mail.py +0 -0
  91. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/minidiamond.py +0 -0
  92. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/none.py +0 -0
  93. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/note.py +0 -0
  94. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/roundedbox.py +0 -0
  95. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/square.py +0 -0
  96. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/noderenderer/textbox.py +0 -0
  97. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/parser.py +0 -0
  98. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/plugins/__init__.py +0 -0
  99. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/plugins/attributes.py +0 -0
  100. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/plugins/autoclass.py +0 -0
  101. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/__init__.py +0 -0
  102. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/bootstrap.py +0 -0
  103. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/compat.py +0 -0
  104. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/config.py +0 -0
  105. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/fontmap.py +0 -0
  106. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/images.py +0 -0
  107. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/logging.py +0 -0
  108. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/myitertools.py +0 -0
  109. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/rst/__init__.py +0 -0
  110. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/rst/directives.py +0 -0
  111. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/rst/nodes.py +0 -0
  112. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/urlutil.py +0 -0
  113. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs/vendor/blockdiag/utils/uuid.py +0 -0
  114. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs.egg-info/dependency_links.txt +0 -0
  115. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs.egg-info/entry_points.txt +0 -0
  116. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs.egg-info/requires.txt +0 -0
  117. {esp_docs-2.1.5 → esp_docs-2.2.0}/src/esp_docs.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: esp-docs
3
- Version: 2.1.5
3
+ Version: 2.2.0
4
4
  Summary: Documentation building package used at Espressif
5
5
  Home-page: https://github.com/espressif/esp-docs
6
6
  Author: Espressif
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=42",
4
+ "wheel"
5
+ ]
6
+ build-backend = "setuptools.build_meta"
7
+
8
+ [tool.commitizen]
9
+ name = "cz_conventional_commits"
10
+ version = "2.2.0"
11
+ tag_format = "v$version"
12
+ version_files = [
13
+ "setup.cfg:version",
14
+ ]
15
+ update_changelog_on_bump = true
16
+ changelog_start_rev = "v2.1.4"
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = esp-docs
3
- version = 2.1.5
3
+ version = 2.2.0
4
4
  author = Espressif
5
5
  author_email = marius.vikhammer@espressif.com
6
6
  description = Documentation building package used at Espressif
@@ -36,6 +36,7 @@ import subprocess
36
36
  import sys
37
37
  from pathlib import Path
38
38
  from .check_docs import check_docs
39
+ from .check_lang_switch import run_lang_linkcheck
39
40
  from esp_docs.constants import TARGETS
40
41
 
41
42
  LANGUAGES = ['en', 'zh_CN']
@@ -97,6 +98,8 @@ def main():
97
98
 
98
99
  action_parsers.add_parser('gh-linkcheck', help='Checking for hardcoded GitHub links')
99
100
 
101
+ action_parsers.add_parser('lang-linkcheck', help='Check if link_to_translation directives are present in RST files included in toctrees')
102
+
100
103
  args = parser.parse_args()
101
104
 
102
105
  global languages
@@ -133,6 +136,9 @@ def main():
133
136
  if args.action == 'gh-linkcheck':
134
137
  sys.exit(action_gh_linkcheck(args))
135
138
 
139
+ if args.action == 'lang-linkcheck':
140
+ sys.exit(action_check_lang_switch(args))
141
+
136
142
 
137
143
  def parallel_call(args, callback):
138
144
  num_sphinx_builds = len(languages) * len(targets)
@@ -414,5 +420,26 @@ def action_gh_linkcheck(args):
414
420
  return 0
415
421
 
416
422
 
423
+ def action_check_lang_switch(args):
424
+ print('Checking for missing translation links\n')
425
+
426
+ # Use the global languages variable set in main(), same as linkcheck
427
+ languages_to_check = languages
428
+ # Use the global targets variable, same as linkcheck
429
+ # When no target is specified, targets = ['generic'], which means no specific target
430
+ if targets == ['generic']:
431
+ target = None
432
+ elif len(targets) == 1:
433
+ target = targets[0]
434
+ else:
435
+ # Multiple targets - for now, check with first target
436
+ # This matches linkcheck behavior which processes all targets
437
+ target = targets[0]
438
+
439
+ # run_lang_linkcheck uses current working directory as docs_dir
440
+ # build-docs should be run from the docs directory
441
+ return run_lang_linkcheck(languages_to_check, target)
442
+
443
+
417
444
  if __name__ == '__main__':
418
445
  main()
@@ -0,0 +1,517 @@
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+ #
4
+ # This script checks that all RST files included in toctree directives have
5
+ # the appropriate :link_to_translation: directive (e.g., :link_to_translation:`zh_CN:[中文]`
6
+ # for English files and :link_to_translation:`en:[English]` for Chinese files).
7
+ #
8
+ # Copyright 2026 Espressif Systems (Shanghai) PTE LTD
9
+ #
10
+ # Licensed under the Apache License, Version 2.0 (the "License");
11
+ # you may not use this file except in compliance with the License.
12
+ # You may obtain a copy of the License at
13
+ #
14
+ # http://www.apache.org/licenses/LICENSE-2.0
15
+ #
16
+ # Unless required by applicable law or agreed to in writing, software
17
+ # distributed under the License is distributed on an "AS IS" BASIS,
18
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ # See the License for the specific language governing permissions and
20
+ # limitations under the License.
21
+
22
+ from __future__ import print_function
23
+
24
+ import fnmatch
25
+ import os
26
+ import re
27
+ import sys
28
+
29
+
30
+ LANGUAGES = ['en', 'zh_CN']
31
+ LINK_TO_TRANSLATION_PATTERN = re.compile(r':link_to_translation:`([^`]+)`')
32
+ TOC_TREE_PATTERN = re.compile(r'\.\.\s+toctree::', re.IGNORECASE)
33
+ TOC_ENTRY_PATTERN = re.compile(r'^\s+([^\s<:]+)', re.MULTILINE)
34
+
35
+
36
+ def find_rst_files(docs_dir, language):
37
+ """Find all RST files in the specified language directory."""
38
+ lang_dir = os.path.join(docs_dir, language)
39
+ if not os.path.exists(lang_dir):
40
+ return []
41
+
42
+ rst_files = []
43
+ for root, dirs, files in os.walk(lang_dir):
44
+ # Skip hidden directories and build directories
45
+ dirs[:] = [d for d in dirs if not d.startswith('.') and d != '_build']
46
+ for file in files:
47
+ if file.endswith('.rst'):
48
+ rst_files.append(os.path.join(root, file))
49
+
50
+ return rst_files
51
+
52
+
53
+ def parse_toctree_entries(rst_file, target=None):
54
+ """Parse toctree entries from an RST file.
55
+
56
+ Args:
57
+ rst_file: Path to the RST file containing the toctree
58
+ target: Target name (e.g., 'esp32', 'esp32s2') to replace {IDF_TARGET_PATH_NAME} macro
59
+ """
60
+ entries = []
61
+
62
+ try:
63
+ with open(rst_file, 'r', encoding='utf-8') as f:
64
+ content = f.read()
65
+ except (IOError, UnicodeDecodeError) as e:
66
+ print("Warning: Could not read file {}: {}".format(rst_file, e), file=sys.stderr)
67
+ return entries
68
+
69
+ # Find all toctree directives
70
+ toctree_matches = list(TOC_TREE_PATTERN.finditer(content))
71
+
72
+ for match in toctree_matches:
73
+ # Find the content block after the toctree directive
74
+ start_pos = match.end()
75
+
76
+ # Find the next directive or non-indented content that ends the toctree
77
+ # Look for lines that are indented (toctree entries) or blank lines
78
+ lines = content[start_pos:].split('\n')
79
+ found_entries = False
80
+
81
+ for i, line in enumerate(lines):
82
+ stripped = line.strip()
83
+
84
+ # Skip blank lines (they're allowed between entries)
85
+ if not stripped:
86
+ continue
87
+
88
+ # Check if this is a new directive (starts with ..) - this ends the toctree
89
+ if stripped.startswith('..'):
90
+ break
91
+
92
+ # Check if this is a toctree option (starts with :)
93
+ if stripped.startswith(':'):
94
+ continue
95
+
96
+ # Check if line is indented (toctree entries are indented)
97
+ # If we've found entries before and this line is not indented, we've reached the end
98
+ if found_entries and not line.startswith((' ', '\t')):
99
+ # Non-indented content line - end of this toctree
100
+ break
101
+
102
+ # Only process indented lines as toctree entries
103
+ if not line.startswith((' ', '\t')):
104
+ continue
105
+
106
+ # This should be a toctree entry
107
+ # First, check if there's a label format: "Label <entry>" or just "entry"
108
+ stripped_line = stripped
109
+
110
+ # Handle entries with labels like "Page One <page1>"
111
+ if '<' in stripped_line and '>' in stripped_line:
112
+ # Extract the entry from inside < >
113
+ entry_match = re.search(r'<([^>]+)>', stripped_line)
114
+ if entry_match:
115
+ entry = entry_match.group(1).strip()
116
+ else:
117
+ # Fallback: use everything before <
118
+ entry = stripped_line.split('<')[0].strip()
119
+ else:
120
+ # Handle entries with filter tags like ":filter: entry"
121
+ if ':' in stripped_line and not stripped_line.startswith(':'):
122
+ # Check if it's a filter tag
123
+ parts = stripped_line.split(':', 1)
124
+ if len(parts) == 2:
125
+ entry = parts[1].strip()
126
+ else:
127
+ entry = stripped_line
128
+ else:
129
+ # Simple entry without label or filter
130
+ # Use the whole stripped line as the entry
131
+ entry = stripped_line
132
+
133
+ # Clean up the entry
134
+ entry = entry.strip()
135
+
136
+ # Replace {IDF_TARGET_PATH_NAME} macro with target value if provided
137
+ if target and '{IDF_TARGET_PATH_NAME}' in entry:
138
+ entry = entry.replace('{IDF_TARGET_PATH_NAME}', target)
139
+
140
+ # Remove .rst extension if present
141
+ if entry.endswith('.rst'):
142
+ entry = entry[:-4]
143
+
144
+ if entry:
145
+ entries.append(entry)
146
+ found_entries = True
147
+
148
+ return entries
149
+
150
+
151
+ def resolve_toctree_entry(entry, base_dir, language, containing_file=None):
152
+ """Resolve a toctree entry to an actual RST file path.
153
+
154
+ Args:
155
+ entry: The toctree entry (e.g., 'page1', 'folder/page1')
156
+ base_dir: Base docs directory (e.g., 'docs')
157
+ language: Language directory (e.g., 'en')
158
+ containing_file: Path to the RST file containing the toctree (optional)
159
+
160
+ Returns:
161
+ Resolved absolute path to the RST file, or None if not found
162
+ """
163
+ # Remove .rst extension if present
164
+ if entry.endswith('.rst'):
165
+ entry = entry[:-4]
166
+
167
+ # Determine the base directory for resolving relative paths
168
+ if containing_file:
169
+ # Use the directory of the containing file as the base
170
+ containing_dir = os.path.dirname(containing_file)
171
+ # Remove the language directory from the path to get the relative base
172
+ # containing_file is like: docs/en/index.rst or docs/en/folder/index.rst
173
+ # containing_dir is like: docs/en or docs/en/folder
174
+ # We want to resolve relative to this directory
175
+ if '/' in entry:
176
+ # Relative path like 'folder/page1' - resolve relative to containing_dir
177
+ rst_path = os.path.join(containing_dir, entry + '.rst')
178
+ else:
179
+ # Simple entry like 'page1' - same directory as containing file
180
+ rst_path = os.path.join(containing_dir, entry + '.rst')
181
+ else:
182
+ # Fallback: resolve relative to language directory
183
+ if '/' in entry:
184
+ # Relative path from language directory
185
+ rst_path = os.path.join(base_dir, language, entry + '.rst')
186
+ else:
187
+ # Same directory as language directory
188
+ rst_path = os.path.join(base_dir, language, entry + '.rst')
189
+
190
+ # Normalize the path
191
+ rst_path = os.path.normpath(rst_path)
192
+
193
+ # Check if file exists
194
+ if os.path.exists(rst_path) and os.path.isfile(rst_path):
195
+ return rst_path
196
+
197
+ # Try with index.rst if the entry points to a directory
198
+ # If entry is 'folder' and folder/index.rst exists, use that
199
+ entry_dir = os.path.join(os.path.dirname(rst_path), os.path.basename(entry))
200
+ if os.path.isdir(entry_dir):
201
+ index_path = os.path.join(entry_dir, 'index.rst')
202
+ if os.path.exists(index_path):
203
+ return index_path
204
+
205
+ # Don't fallback to index.rst in the containing directory - that's wrong
206
+ # If the file doesn't exist, return None
207
+ return None
208
+
209
+
210
+ def check_if_translation_only_has_link(translation_file, original_language):
211
+ """Check if translation file only contains an include directive pointing to the original file.
212
+
213
+ When a translation is not ready, the file typically contains only:
214
+ .. include:: ../../en/api-guides/build-system-v2.rst
215
+
216
+ Args:
217
+ translation_file: Path to the translation file
218
+ original_language: Language of the original file ('en' or 'zh_CN')
219
+
220
+ Returns:
221
+ True if the file only contains an include directive to the original, False otherwise
222
+ """
223
+ if not os.path.exists(translation_file):
224
+ return False
225
+
226
+ try:
227
+ with open(translation_file, 'r', encoding='utf-8') as f:
228
+ content = f.read()
229
+ except (IOError, UnicodeDecodeError):
230
+ return False
231
+
232
+ # Remove whitespace and check if content is essentially empty except for include directive
233
+ lines = [line.strip() for line in content.split('\n') if line.strip()]
234
+
235
+ # If file has very few lines, it might be a placeholder
236
+ if len(lines) <= 3:
237
+ # Check if it contains an .. include:: directive pointing to the original language
238
+ # Pattern: .. include:: ../../en/... or .. include:: ../en/... etc.
239
+ include_pattern = re.compile(r'\.\.\s+include::\s+(.+)', re.IGNORECASE)
240
+ matches = include_pattern.findall(content)
241
+
242
+ for match in matches:
243
+ include_path = match.strip()
244
+ # Check if the include path points to the original language folder
245
+ # Paths like: ../../en/..., ../en/..., en/..., /path/to/en/...
246
+ if '/' + original_language + '/' in include_path or include_path.startswith(original_language + '/'):
247
+ # This is a placeholder file that only includes the original
248
+ return True
249
+
250
+ return False
251
+
252
+
253
+ def check_link_to_translation(rst_file, expected_language, docs_dir):
254
+ """Check if an RST file has the appropriate link_to_translation directive.
255
+
256
+ Args:
257
+ rst_file: Path to the RST file to check
258
+ expected_language: Expected language in the link (e.g., 'zh_CN' for English files)
259
+ docs_dir: Base docs directory for resolving translation file paths
260
+ """
261
+ try:
262
+ with open(rst_file, 'r', encoding='utf-8') as f:
263
+ content = f.read()
264
+ except (IOError, UnicodeDecodeError) as e:
265
+ return False, "Could not read file: {}".format(e)
266
+
267
+ # Find all link_to_translation directives
268
+ matches = LINK_TO_TRANSLATION_PATTERN.findall(content)
269
+
270
+ if not matches:
271
+ # Check if translation file doesn't exist, or if current file or translation file is a placeholder
272
+ # Get the language of the current file
273
+ rel_path = os.path.relpath(rst_file, docs_dir)
274
+ if rel_path.startswith('en/'):
275
+ current_language = 'en'
276
+ translation_language = 'zh_CN'
277
+ elif rel_path.startswith('zh_CN/'):
278
+ current_language = 'zh_CN'
279
+ translation_language = 'en'
280
+ else:
281
+ # Can't determine, report error
282
+ return False, "Missing :link_to_translation: directive"
283
+
284
+ # Check if the current file itself is a placeholder (includes the translation language file)
285
+ if check_if_translation_only_has_link(rst_file, translation_language):
286
+ # Current file is a placeholder that includes the translation, don't report error
287
+ return True, None
288
+
289
+ # Check if translation file doesn't exist
290
+ translation_path = rst_file.replace('/' + current_language + '/', '/' + translation_language + '/')
291
+ if not os.path.exists(translation_path):
292
+ # Translation file doesn't exist, don't report error
293
+ return True, None
294
+
295
+ # Check if translation file is a placeholder (includes the current/original language file)
296
+ if check_if_translation_only_has_link(translation_path, current_language):
297
+ # Translation file is a placeholder that includes the original, don't report error
298
+ return True, None
299
+
300
+ return False, "Missing :link_to_translation: directive"
301
+
302
+ # Check for multiple links - should only have one
303
+ if len(matches) > 1:
304
+ return False, "Multiple :link_to_translation: directives found (expected exactly one)"
305
+
306
+ # Check if the match has the expected language
307
+ match = matches[0]
308
+ # Parse the language from the match (format: "zh_CN:[中文]" or "en:[English]")
309
+ parts = match.split(':', 1)
310
+ if len(parts) >= 1:
311
+ lang = parts[0].strip()
312
+ if lang == expected_language:
313
+ return True, None
314
+ else:
315
+ return False, "Incorrect :link_to_translation: directive (found '{}', expected '{}')".format(lang, expected_language)
316
+
317
+ return False, "Missing :link_to_translation:`{}:` directive".format(expected_language)
318
+
319
+
320
+ def load_excluded_files_from_warnings(docs_dir):
321
+ """Load list of files to ignore from lang-linkcheck-warnings.txt.
322
+
323
+ The warnings file should contain one file path per line (relative to docs_dir,
324
+ with language prefix and .rst extension, e.g., "en/page.rst" or "zh_CN/folder/page.rst").
325
+ Lines starting with # are treated as comments and ignored.
326
+
327
+ Wildcards are supported: use * to match any string and ? to match any single character.
328
+ Examples:
329
+ en/getting-started/* matches all .rst under en/getting-started/
330
+ en/**/internal.rst (fnmatch does not support **; use * for one segment)
331
+ zh_CN/api-reference/*.rst matches all .rst in zh_CN/api-reference/
332
+
333
+ Args:
334
+ docs_dir: Base docs directory
335
+
336
+ Returns:
337
+ Tuple of (exact_paths, patterns) where exact_paths is a set of literal paths to ignore,
338
+ and patterns is a list of fnmatch patterns (entries containing * or ?).
339
+ """
340
+ exact_paths = set()
341
+ patterns = []
342
+ warnings_file = os.path.join(docs_dir, 'lang-linkcheck-warnings.txt')
343
+
344
+ if not os.path.exists(warnings_file):
345
+ return exact_paths, patterns
346
+
347
+ try:
348
+ with open(warnings_file, 'r', encoding='utf-8') as f:
349
+ for line in f:
350
+ # Strip whitespace and skip empty lines and comments
351
+ line = line.strip()
352
+ if not line or line.startswith('#'):
353
+ continue
354
+
355
+ # Normalize path separators
356
+ normalized = line.replace('\\', '/')
357
+ if '*' in normalized or '?' in normalized:
358
+ patterns.append(normalized)
359
+ else:
360
+ exact_paths.add(normalized)
361
+ except (IOError, UnicodeDecodeError) as e:
362
+ if os.environ.get('DEBUG_LANG_LINKCHECK', ''):
363
+ print("Warning: Could not read lang-linkcheck-warnings.txt: {}".format(e), file=sys.stderr)
364
+
365
+ return exact_paths, patterns
366
+
367
+
368
+ def check_lang_switch(docs_dir, language='en', target=None):
369
+ """Check translation links for files in toctrees for a given language.
370
+
371
+ Args:
372
+ docs_dir: Base docs directory
373
+ language: Language to check ('en' or 'zh_CN')
374
+ target: Target name (e.g., 'esp32', 'esp32s2') to replace {IDF_TARGET_PATH_NAME} macro
375
+
376
+ Returns:
377
+ Tuple of (errors, passed_files) where:
378
+ - errors: List of (file_path, error_msg) tuples for files missing translation links
379
+ - passed_files: List of file paths that passed the check
380
+ """
381
+ errors = []
382
+ passed_files = []
383
+ checked_files = set()
384
+
385
+ # Load excluded files from warnings file (exact paths and wildcard patterns)
386
+ ignored_exact, ignored_patterns = load_excluded_files_from_warnings(docs_dir)
387
+
388
+ # Find all RST files in this language
389
+ rst_files = find_rst_files(docs_dir, language)
390
+
391
+ # Determine the other language
392
+ other_language = 'zh_CN' if language == 'en' else 'en'
393
+
394
+ # Collect all files referenced in toctrees
395
+ toctree_files = set()
396
+
397
+ for rst_file in rst_files:
398
+ entries = parse_toctree_entries(rst_file, target)
399
+ if entries:
400
+ rel_rst_file = os.path.relpath(rst_file, docs_dir)
401
+ # Debug: show what was parsed from each file
402
+ if os.environ.get('DEBUG_TOCTREE', ''):
403
+ print(f" [{language}] {rel_rst_file}: found {len(entries)} toctree entries: {entries}")
404
+ for entry in entries:
405
+ resolved_file = resolve_toctree_entry(entry, docs_dir, language, rst_file)
406
+ if resolved_file and os.path.exists(resolved_file):
407
+ toctree_files.add(resolved_file)
408
+ elif resolved_file:
409
+ # File path was resolved but doesn't exist - might be a macro issue
410
+ rel_rst_file = os.path.relpath(rst_file, docs_dir)
411
+ if os.environ.get('DEBUG_TOCTREE', ''):
412
+ print(f" Warning: Resolved '{entry}' -> {os.path.relpath(resolved_file, docs_dir)} (not found)")
413
+
414
+ # Always check index.rst files (homepages) even if they're not in a toctree
415
+ lang_dir = os.path.join(docs_dir, language)
416
+ index_file = os.path.join(lang_dir, 'index.rst')
417
+ if os.path.exists(index_file):
418
+ toctree_files.add(index_file)
419
+
420
+ # Check each file in toctree
421
+ for rst_file in toctree_files:
422
+ if rst_file in checked_files:
423
+ continue
424
+ checked_files.add(rst_file)
425
+
426
+ rel_path = os.path.relpath(rst_file, docs_dir)
427
+
428
+ # Check if this file should be ignored (exact path or wildcard pattern)
429
+ # rel_path is like "en/page.rst" or "zh_CN/folder/page.rst"
430
+ if rel_path in ignored_exact:
431
+ continue
432
+ if any(fnmatch.fnmatch(rel_path, p) for p in ignored_patterns):
433
+ continue
434
+
435
+ has_link, error_msg = check_link_to_translation(rst_file, other_language, docs_dir)
436
+ if not has_link:
437
+ errors.append((rel_path, error_msg))
438
+ else:
439
+ passed_files.append(rel_path)
440
+
441
+ return errors, passed_files
442
+
443
+
444
+ def run_lang_linkcheck(languages_to_check, target=None):
445
+ """Run the language link check and print results.
446
+
447
+ This function runs from the current working directory, which should be the docs directory.
448
+
449
+ Args:
450
+ languages_to_check: List of languages to check (e.g., ['en', 'zh_CN'])
451
+ target: Target name (e.g., 'esp32', 'esp32s2') to replace {IDF_TARGET_PATH_NAME} macro
452
+
453
+ Returns:
454
+ Exit code: 0 if successful, 1 if errors found
455
+ """
456
+ # Use current working directory as docs directory
457
+ docs_dir = os.getcwd()
458
+
459
+ if not os.path.exists(docs_dir):
460
+ print("Error: Current directory '{}' does not exist".format(docs_dir), file=sys.stderr)
461
+ return 1
462
+
463
+ all_errors = []
464
+ all_passed_files = []
465
+ errors_by_language = {'en': [], 'zh_CN': []}
466
+
467
+ for language in languages_to_check:
468
+ errors, passed_files = check_lang_switch(docs_dir, language, target)
469
+ errors_by_language[language] = errors
470
+ all_errors.extend(errors)
471
+ all_passed_files.extend(passed_files)
472
+
473
+ # Print results organized by language and missing link type
474
+ print("\n" + "="*80)
475
+ print("TRANSLATION LINK CHECK RESULTS")
476
+ print("="*80)
477
+
478
+ # List files missing Chinese translation links
479
+ en_missing_zh = [e for lang in ['en'] if lang in languages_to_check for e in errors_by_language[lang]]
480
+ if en_missing_zh:
481
+ print("\n[EN] Files missing link to Chinese translation ({}):".format(len(en_missing_zh)))
482
+ for file_path, error_msg in en_missing_zh:
483
+ print(" ✗ {}: {}".format(file_path, error_msg))
484
+ else:
485
+ if 'en' in languages_to_check:
486
+ print("\n[EN] ✓ All files have link to Chinese translation")
487
+
488
+ # List files missing English translation links
489
+ zh_missing_en = [e for lang in ['zh_CN'] if lang in languages_to_check for e in errors_by_language[lang]]
490
+ if zh_missing_en:
491
+ print("\n[ZH_CN] Files missing link to English translation ({}):".format(len(zh_missing_en)))
492
+ for file_path, error_msg in zh_missing_en:
493
+ print(" ✗ {}: {}".format(file_path, error_msg))
494
+ else:
495
+ if 'zh_CN' in languages_to_check:
496
+ print("\n[ZH_CN] ✓ All files have link to English translation")
497
+
498
+ # List files that passed the check
499
+ if all_passed_files:
500
+ print("\n[PASSED] Files with correct translation links ({}):".format(len(all_passed_files)))
501
+ for file_path in sorted(all_passed_files):
502
+ print(" ✓ {}".format(file_path))
503
+
504
+ if all_errors:
505
+ print("\n" + "="*80)
506
+ print("ERROR: Found {} file(s) missing translation links.".format(len(all_errors)))
507
+ print("Please add the appropriate :link_to_translation: directive to these files.")
508
+ print("\nExample:")
509
+ print(" For English files: :link_to_translation:`zh_CN:[中文]`")
510
+ print(" For Chinese files: :link_to_translation:`en:[English]`")
511
+ print("="*80)
512
+ return 1
513
+ else:
514
+ print("\n" + "="*80)
515
+ print("SUCCESS: All files in toctrees have translation links.")
516
+ print("="*80)
517
+ return 0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: esp-docs
3
- Version: 2.1.5
3
+ Version: 2.2.0
4
4
  Summary: Documentation building package used at Espressif
5
5
  Home-page: https://github.com/espressif/esp-docs
6
6
  Author: Espressif
@@ -5,6 +5,7 @@ setup.py
5
5
  src/esp_docs/__init__.py
6
6
  src/esp_docs/build_docs.py
7
7
  src/esp_docs/check_docs.py
8
+ src/esp_docs/check_lang_switch.py
8
9
  src/esp_docs/conf_docs.py
9
10
  src/esp_docs/constants.py
10
11
  src/esp_docs/deploy_docs.py
@@ -1,6 +0,0 @@
1
- [build-system]
2
- requires = [
3
- "setuptools>=42",
4
- "wheel"
5
- ]
6
- build-backend = "setuptools.build_meta"
File without changes
File without changes