sphinx-llm-friendly 0.1.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.
- sphinx_llm_friendly-0.1.0/.gitignore +6 -0
- sphinx_llm_friendly-0.1.0/LICENSE +23 -0
- sphinx_llm_friendly-0.1.0/PKG-INFO +100 -0
- sphinx_llm_friendly-0.1.0/README.rst +70 -0
- sphinx_llm_friendly-0.1.0/pyproject.toml +141 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/__init__.py +223 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/_build.py +90 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/_builder.py +99 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/_contexts.py +448 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/_escape.py +13 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/_exclude.py +27 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/_intersphinx.py +101 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/_llm.py +56 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/_llms_txt.py +81 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/_singlemarkdown.py +112 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/_translator.py +907 -0
- sphinx_llm_friendly-0.1.0/sphinx_llm_friendly/_writer.py +27 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/ExampleRSTFile.md +596 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/Section_course_student.md +82 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/auto-module.md +67 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/auto-summery.md +11 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/blocks.md +55 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/empty.md +1 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/glossaries.md +25 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/image-target.md +7 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/index.md +76 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/library/my_module.md +71 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/library/my_module.module_class.md +34 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/library/my_module.submodule.md +7 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/library/my_module.submodule.my_class.md +26 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llm_markdown/links.md +0 -0
- sphinx_llm_friendly-0.1.0/tests/expected/llms-full.txt +1017 -0
- sphinx_llm_friendly-0.1.0/tests/my_module/__init__.py +62 -0
- sphinx_llm_friendly-0.1.0/tests/my_module/module_class.py +35 -0
- sphinx_llm_friendly-0.1.0/tests/my_module/submodule/__init__.py +3 -0
- sphinx_llm_friendly-0.1.0/tests/my_module/submodule/my_class.py +28 -0
- sphinx_llm_friendly-0.1.0/tests/source/ExampleRSTFile.rst +806 -0
- sphinx_llm_friendly-0.1.0/tests/source/Section_course_student.rst +128 -0
- sphinx_llm_friendly-0.1.0/tests/source/_templates/custom-module-template.rst +18 -0
- sphinx_llm_friendly-0.1.0/tests/source/auto-module.rst +5 -0
- sphinx_llm_friendly-0.1.0/tests/source/auto-summery.rst +29 -0
- sphinx_llm_friendly-0.1.0/tests/source/blocks.rst +73 -0
- sphinx_llm_friendly-0.1.0/tests/source/conf.py +27 -0
- sphinx_llm_friendly-0.1.0/tests/source/empty.rst +15 -0
- sphinx_llm_friendly-0.1.0/tests/source/glossaries.rst +38 -0
- sphinx_llm_friendly-0.1.0/tests/source/image-target.rst +11 -0
- sphinx_llm_friendly-0.1.0/tests/source/index.rst +15 -0
- sphinx_llm_friendly-0.1.0/tests/source/links.rst +638 -0
- sphinx_llm_friendly-0.1.0/tests/source/static/markdown.png +0 -0
- sphinx_llm_friendly-0.1.0/tests/test_extension.py +167 -0
- sphinx_llm_friendly-0.1.0/tests/test_markdown.py +106 -0
- sphinx_llm_friendly-0.1.0/tests/test_singlemarkdown.py +84 -0
- sphinx_llm_friendly-0.1.0/tests/test_translator.py +161 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Scrapy developers.
|
|
4
|
+
Copyright (c) 2023-2026, Liran Funaro.
|
|
5
|
+
Copyright (c) 2025 Jared Dillard
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sphinx-llm-friendly
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sphinx extension to make documentation LLM-friendly
|
|
5
|
+
Project-URL: Source, https://github.com/scrapy/sphinx-llm-friendly
|
|
6
|
+
Project-URL: Tracker, https://github.com/scrapy/sphinx-llm-friendly/issues
|
|
7
|
+
Project-URL: Release notes, https://github.com/scrapy/sphinx-llm-friendly/blob/main/CHANGES.rst
|
|
8
|
+
Author-email: Scrapy developers <opensource@zyte.com>
|
|
9
|
+
Maintainer-email: Scrapy developers <opensource@zyte.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: Sphinx :: Extension
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
24
|
+
Classifier: Topic :: Documentation :: Sphinx
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: docutils
|
|
27
|
+
Requires-Dist: sphinx>=7.4
|
|
28
|
+
Requires-Dist: tabulate
|
|
29
|
+
Description-Content-Type: text/x-rst
|
|
30
|
+
|
|
31
|
+
===================
|
|
32
|
+
sphinx-llm-friendly
|
|
33
|
+
===================
|
|
34
|
+
|
|
35
|
+
`Sphinx <https://www.sphinx-doc.org/>`_ extension that makes documentation
|
|
36
|
+
LLM-friendly:
|
|
37
|
+
|
|
38
|
+
- A Markdown version of every page, next to its HTML version.
|
|
39
|
+
|
|
40
|
+
- `llms.txt <https://llmstxt.org/>`_, with a link to the Markdown version of
|
|
41
|
+
every page.
|
|
42
|
+
|
|
43
|
+
- ``llms-full.txt``, with the Markdown version of all pages.
|
|
44
|
+
|
|
45
|
+
- HTML pages point to their Markdown version with a ``<link
|
|
46
|
+
rel="alternate" type="text/markdown">`` tag, and get a button to copy
|
|
47
|
+
their Markdown version.
|
|
48
|
+
|
|
49
|
+
Setup
|
|
50
|
+
=====
|
|
51
|
+
|
|
52
|
+
#. Install it:
|
|
53
|
+
|
|
54
|
+
.. code-block:: shell
|
|
55
|
+
|
|
56
|
+
pip install sphinx-llm-friendly
|
|
57
|
+
|
|
58
|
+
#. Add it to ``extensions`` in ``conf.py``:
|
|
59
|
+
|
|
60
|
+
.. code-block:: python
|
|
61
|
+
|
|
62
|
+
extensions = [
|
|
63
|
+
# …
|
|
64
|
+
"sphinx_llm_friendly",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
#. Build your documentation:
|
|
68
|
+
|
|
69
|
+
.. code-block:: shell
|
|
70
|
+
|
|
71
|
+
sphinx-llm-friendly build docs docs/_build
|
|
72
|
+
|
|
73
|
+
The output is in ``docs/_build/all``.
|
|
74
|
+
|
|
75
|
+
It runs the ``html``, ``llm_markdown`` and ``llm_singlemarkdown`` builders
|
|
76
|
+
in parallel, into subdirectories of ``docs/_build``, and merges their
|
|
77
|
+
output. In the Markdown output, links to sites from ``intersphinx_mapping``
|
|
78
|
+
that serve Markdown point to the Markdown version of their pages.
|
|
79
|
+
|
|
80
|
+
From Python, call ``sphinx_llm_friendly.build(source_dir, build_dir)``,
|
|
81
|
+
which returns the output directory.
|
|
82
|
+
|
|
83
|
+
Configuration
|
|
84
|
+
=============
|
|
85
|
+
|
|
86
|
+
``llm_friendly_exclude``
|
|
87
|
+
List of patterns, with the syntax of ``exclude_patterns``, of documents to
|
|
88
|
+
leave out of the Markdown output and ``llms.txt``. Default: ``[]``.
|
|
89
|
+
|
|
90
|
+
``llm_friendly_llms_txt_summary``
|
|
91
|
+
Summary for ``llms.txt``. Default: the first paragraph of the root
|
|
92
|
+
document.
|
|
93
|
+
|
|
94
|
+
``llm_friendly_llms_txt_toctree_only``
|
|
95
|
+
If ``True``, ``llms.txt`` only lists documents reachable through toctrees
|
|
96
|
+
from the root document. Default: ``False``.
|
|
97
|
+
|
|
98
|
+
Nodes from third-party extensions need Markdown handlers, registered with
|
|
99
|
+
``app.add_node()`` for the ``llm_markdown`` and ``llm_singlemarkdown``
|
|
100
|
+
builders.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
===================
|
|
2
|
+
sphinx-llm-friendly
|
|
3
|
+
===================
|
|
4
|
+
|
|
5
|
+
`Sphinx <https://www.sphinx-doc.org/>`_ extension that makes documentation
|
|
6
|
+
LLM-friendly:
|
|
7
|
+
|
|
8
|
+
- A Markdown version of every page, next to its HTML version.
|
|
9
|
+
|
|
10
|
+
- `llms.txt <https://llmstxt.org/>`_, with a link to the Markdown version of
|
|
11
|
+
every page.
|
|
12
|
+
|
|
13
|
+
- ``llms-full.txt``, with the Markdown version of all pages.
|
|
14
|
+
|
|
15
|
+
- HTML pages point to their Markdown version with a ``<link
|
|
16
|
+
rel="alternate" type="text/markdown">`` tag, and get a button to copy
|
|
17
|
+
their Markdown version.
|
|
18
|
+
|
|
19
|
+
Setup
|
|
20
|
+
=====
|
|
21
|
+
|
|
22
|
+
#. Install it:
|
|
23
|
+
|
|
24
|
+
.. code-block:: shell
|
|
25
|
+
|
|
26
|
+
pip install sphinx-llm-friendly
|
|
27
|
+
|
|
28
|
+
#. Add it to ``extensions`` in ``conf.py``:
|
|
29
|
+
|
|
30
|
+
.. code-block:: python
|
|
31
|
+
|
|
32
|
+
extensions = [
|
|
33
|
+
# …
|
|
34
|
+
"sphinx_llm_friendly",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
#. Build your documentation:
|
|
38
|
+
|
|
39
|
+
.. code-block:: shell
|
|
40
|
+
|
|
41
|
+
sphinx-llm-friendly build docs docs/_build
|
|
42
|
+
|
|
43
|
+
The output is in ``docs/_build/all``.
|
|
44
|
+
|
|
45
|
+
It runs the ``html``, ``llm_markdown`` and ``llm_singlemarkdown`` builders
|
|
46
|
+
in parallel, into subdirectories of ``docs/_build``, and merges their
|
|
47
|
+
output. In the Markdown output, links to sites from ``intersphinx_mapping``
|
|
48
|
+
that serve Markdown point to the Markdown version of their pages.
|
|
49
|
+
|
|
50
|
+
From Python, call ``sphinx_llm_friendly.build(source_dir, build_dir)``,
|
|
51
|
+
which returns the output directory.
|
|
52
|
+
|
|
53
|
+
Configuration
|
|
54
|
+
=============
|
|
55
|
+
|
|
56
|
+
``llm_friendly_exclude``
|
|
57
|
+
List of patterns, with the syntax of ``exclude_patterns``, of documents to
|
|
58
|
+
leave out of the Markdown output and ``llms.txt``. Default: ``[]``.
|
|
59
|
+
|
|
60
|
+
``llm_friendly_llms_txt_summary``
|
|
61
|
+
Summary for ``llms.txt``. Default: the first paragraph of the root
|
|
62
|
+
document.
|
|
63
|
+
|
|
64
|
+
``llm_friendly_llms_txt_toctree_only``
|
|
65
|
+
If ``True``, ``llms.txt`` only lists documents reachable through toctrees
|
|
66
|
+
from the root document. Default: ``False``.
|
|
67
|
+
|
|
68
|
+
Nodes from third-party extensions need Markdown handlers, registered with
|
|
69
|
+
``app.add_node()`` for the ``llm_markdown`` and ``llm_singlemarkdown``
|
|
70
|
+
builders.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sphinx-llm-friendly"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Sphinx extension to make documentation LLM-friendly"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"docutils",
|
|
11
|
+
"sphinx>=7.4",
|
|
12
|
+
"tabulate",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Framework :: Sphinx :: Extension",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Programming Language :: Python :: 3.15",
|
|
27
|
+
"Topic :: Documentation :: Sphinx",
|
|
28
|
+
]
|
|
29
|
+
license = "MIT"
|
|
30
|
+
license-files = ["LICENSE"]
|
|
31
|
+
readme = "README.rst"
|
|
32
|
+
requires-python = ">=3.10"
|
|
33
|
+
authors = [{ name = "Scrapy developers", email = "opensource@zyte.com" }]
|
|
34
|
+
maintainers = [{ name = "Scrapy developers", email = "opensource@zyte.com" }]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
sphinx-llm-friendly = "sphinx_llm_friendly._build:main"
|
|
38
|
+
|
|
39
|
+
[project.entry-points."sphinx.builders"]
|
|
40
|
+
llm_markdown = "sphinx_llm_friendly"
|
|
41
|
+
llm_singlemarkdown = "sphinx_llm_friendly"
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Source = "https://github.com/scrapy/sphinx-llm-friendly"
|
|
45
|
+
Tracker = "https://github.com/scrapy/sphinx-llm-friendly/issues"
|
|
46
|
+
"Release notes" = "https://github.com/scrapy/sphinx-llm-friendly/blob/main/CHANGES.rst"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
include = ["/sphinx_llm_friendly", "/tests"]
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
strict = true
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
|
|
57
|
+
[tool.bumpversion]
|
|
58
|
+
current_version = "0.1.0"
|
|
59
|
+
commit = true
|
|
60
|
+
tag = true
|
|
61
|
+
tag_name = "{new_version}"
|
|
62
|
+
|
|
63
|
+
[[tool.bumpversion.files]]
|
|
64
|
+
filename = "CHANGES.rst"
|
|
65
|
+
search = "\\(unreleased\\)$"
|
|
66
|
+
replace = "({now:%Y-%m-%d})"
|
|
67
|
+
regex = true
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint]
|
|
70
|
+
extend-select = [
|
|
71
|
+
# flake8-bugbear
|
|
72
|
+
"B",
|
|
73
|
+
# flake8-comprehensions
|
|
74
|
+
"C4",
|
|
75
|
+
# pydocstyle
|
|
76
|
+
"D",
|
|
77
|
+
# flake8-future-annotations
|
|
78
|
+
"FA",
|
|
79
|
+
# flynt
|
|
80
|
+
"FLY",
|
|
81
|
+
# refurb
|
|
82
|
+
"FURB",
|
|
83
|
+
# isort
|
|
84
|
+
"I",
|
|
85
|
+
# flake8-implicit-str-concat
|
|
86
|
+
"ISC",
|
|
87
|
+
# flake8-logging
|
|
88
|
+
"LOG",
|
|
89
|
+
# Perflint
|
|
90
|
+
"PERF",
|
|
91
|
+
# pygrep-hooks
|
|
92
|
+
"PGH",
|
|
93
|
+
# flake8-pie
|
|
94
|
+
"PIE",
|
|
95
|
+
# pylint
|
|
96
|
+
"PL",
|
|
97
|
+
# flake8-pytest-style
|
|
98
|
+
"PT",
|
|
99
|
+
# flake8-use-pathlib
|
|
100
|
+
"PTH",
|
|
101
|
+
# flake8-pyi
|
|
102
|
+
"PYI",
|
|
103
|
+
# flake8-quotes
|
|
104
|
+
"Q",
|
|
105
|
+
# flake8-return
|
|
106
|
+
"RET",
|
|
107
|
+
# flake8-raise
|
|
108
|
+
"RSE",
|
|
109
|
+
# Ruff-specific rules
|
|
110
|
+
"RUF",
|
|
111
|
+
# flake8-bandit
|
|
112
|
+
"S",
|
|
113
|
+
# flake8-simplify
|
|
114
|
+
"SIM",
|
|
115
|
+
# flake8-slots
|
|
116
|
+
"SLOT",
|
|
117
|
+
# flake8-debugger
|
|
118
|
+
"T10",
|
|
119
|
+
# flake8-type-checking
|
|
120
|
+
"TC",
|
|
121
|
+
# flake8-tidy-imports
|
|
122
|
+
"TID",
|
|
123
|
+
# pyupgrade
|
|
124
|
+
"UP",
|
|
125
|
+
# pycodestyle warnings
|
|
126
|
+
"W",
|
|
127
|
+
# flake8-2020
|
|
128
|
+
"YTT",
|
|
129
|
+
]
|
|
130
|
+
ignore = [
|
|
131
|
+
# No public API to document
|
|
132
|
+
"D",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
[tool.ruff.lint.per-file-ignores]
|
|
136
|
+
"sphinx_llm_friendly/_contexts.py" = ["B008", "PT018", "S101"]
|
|
137
|
+
"sphinx_llm_friendly/_translator.py" = ["PLR0911", "S101"]
|
|
138
|
+
"tests/*" = ["S101"]
|
|
139
|
+
|
|
140
|
+
[tool.ruff.lint.pydocstyle]
|
|
141
|
+
convention = "pep257"
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import posixpath
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from sphinx.util.osutil import relative_uri
|
|
8
|
+
|
|
9
|
+
from ._build import build, remember_intersphinx_urls
|
|
10
|
+
from ._builder import MarkdownBuilder
|
|
11
|
+
from ._exclude import is_excluded
|
|
12
|
+
from ._llms_txt import write_llms_txt
|
|
13
|
+
from ._singlemarkdown import SingleFileMarkdownBuilder
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from docutils import nodes
|
|
17
|
+
from sphinx.application import Sphinx
|
|
18
|
+
from sphinx.util.typing import ExtensionMetadata
|
|
19
|
+
|
|
20
|
+
__all__ = ["build", "setup"]
|
|
21
|
+
|
|
22
|
+
_COPY_AS_MARKDOWN_BUTTON_JS = """
|
|
23
|
+
(function () {
|
|
24
|
+
var DEFAULT_LABEL = 'M\u2193';
|
|
25
|
+
var SUCCESS_LABEL = 'Copied';
|
|
26
|
+
var ERROR_LABEL = 'Error';
|
|
27
|
+
|
|
28
|
+
function markdownPathFromCurrentPage(pathname) {
|
|
29
|
+
if (pathname.endsWith('.html')) {
|
|
30
|
+
return pathname.slice(0, -5) + '.md';
|
|
31
|
+
}
|
|
32
|
+
if (pathname.endsWith('/')) {
|
|
33
|
+
return pathname + 'index.md';
|
|
34
|
+
}
|
|
35
|
+
var lastPart = pathname.split('/').pop() || '';
|
|
36
|
+
if (!lastPart.includes('.')) {
|
|
37
|
+
return pathname + '.md';
|
|
38
|
+
}
|
|
39
|
+
return pathname + '.md';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function copyToClipboard(text) {
|
|
43
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
44
|
+
await navigator.clipboard.writeText(text);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
var textarea = document.createElement('textarea');
|
|
48
|
+
textarea.value = text;
|
|
49
|
+
textarea.setAttribute('readonly', 'readonly');
|
|
50
|
+
textarea.style.position = 'fixed';
|
|
51
|
+
textarea.style.left = '-9999px';
|
|
52
|
+
document.body.appendChild(textarea);
|
|
53
|
+
textarea.select();
|
|
54
|
+
document.execCommand('copy');
|
|
55
|
+
document.body.removeChild(textarea);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setTemporaryLabel(button, label, previousLabel) {
|
|
59
|
+
var prev = typeof previousLabel !== 'undefined' ? previousLabel : button.textContent;
|
|
60
|
+
button.textContent = label;
|
|
61
|
+
window.setTimeout(function () {
|
|
62
|
+
button.textContent = prev;
|
|
63
|
+
button.disabled = false;
|
|
64
|
+
}, 1000);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function onButtonClick(button) {
|
|
68
|
+
var previousLabel = button.textContent;
|
|
69
|
+
button.disabled = true;
|
|
70
|
+
button.textContent = '...';
|
|
71
|
+
try {
|
|
72
|
+
var mdPath = markdownPathFromCurrentPage(window.location.pathname);
|
|
73
|
+
var response = await fetch(mdPath, { credentials: 'same-origin' });
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error('Unable to fetch markdown source');
|
|
76
|
+
}
|
|
77
|
+
var markdown = await response.text();
|
|
78
|
+
await copyToClipboard(markdown);
|
|
79
|
+
setTemporaryLabel(button, SUCCESS_LABEL, previousLabel);
|
|
80
|
+
} catch (_error) {
|
|
81
|
+
setTemporaryLabel(button, ERROR_LABEL, previousLabel);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function addStyles() {
|
|
86
|
+
var style = document.createElement('style');
|
|
87
|
+
style.textContent = [
|
|
88
|
+
'.scrapy-copy-as-markdown {',
|
|
89
|
+
' display: inline-block;',
|
|
90
|
+
' margin-left: 0.25rem;',
|
|
91
|
+
' border: 1px solid #c9d4de;',
|
|
92
|
+
' border-radius: 0.45rem;',
|
|
93
|
+
' background: #ffffff;',
|
|
94
|
+
' color: #233a50;',
|
|
95
|
+
' font: inherit;',
|
|
96
|
+
' font-size: 0.875rem;',
|
|
97
|
+
' line-height: 1;',
|
|
98
|
+
' padding: 0.25rem 0.25rem;',
|
|
99
|
+
' box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);',
|
|
100
|
+
' cursor: pointer;',
|
|
101
|
+
'}',
|
|
102
|
+
'.scrapy-copy-as-markdown:hover {',
|
|
103
|
+
' background: #f3f7fb;',
|
|
104
|
+
'}',
|
|
105
|
+
'.scrapy-copy-as-markdown:disabled {',
|
|
106
|
+
' opacity: 0.75;',
|
|
107
|
+
' cursor: default;',
|
|
108
|
+
'}',
|
|
109
|
+
'.scrapy-copy-as-markdown-title-wrapper {',
|
|
110
|
+
' display: flex;',
|
|
111
|
+
' align-items: center;',
|
|
112
|
+
' justify-content: space-between;',
|
|
113
|
+
' gap: 1rem;',
|
|
114
|
+
' width: 100%;',
|
|
115
|
+
'}',
|
|
116
|
+
'.scrapy-copy-as-markdown-title-wrapper h1 {',
|
|
117
|
+
' margin: 0;',
|
|
118
|
+
'}',
|
|
119
|
+
].join('\\n');
|
|
120
|
+
document.head.appendChild(style);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function addButton() {
|
|
124
|
+
if (!document.body || document.querySelector('.scrapy-copy-as-markdown')) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
addStyles();
|
|
129
|
+
var button = document.createElement('button');
|
|
130
|
+
button.type = 'button';
|
|
131
|
+
button.className = 'scrapy-copy-as-markdown';
|
|
132
|
+
button.title = 'Copy this page as Markdown';
|
|
133
|
+
button.setAttribute('aria-label', 'Copy this page as Markdown');
|
|
134
|
+
button.textContent = DEFAULT_LABEL;
|
|
135
|
+
button.addEventListener('click', function () {
|
|
136
|
+
onButtonClick(button);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
var h1 = document.querySelector('#main h1') || document.querySelector('h1');
|
|
140
|
+
if (h1 && h1.parentNode) {
|
|
141
|
+
var parent = h1.parentNode;
|
|
142
|
+
var wrapper = document.createElement('div');
|
|
143
|
+
wrapper.className = 'scrapy-copy-as-markdown-title-wrapper';
|
|
144
|
+
parent.replaceChild(wrapper, h1);
|
|
145
|
+
wrapper.appendChild(h1);
|
|
146
|
+
wrapper.appendChild(button);
|
|
147
|
+
} else {
|
|
148
|
+
// fallback: insert at top of body but keep within the first container
|
|
149
|
+
var container = document.body.firstElementChild || document.body;
|
|
150
|
+
container.insertBefore(button, container.firstChild);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (document.readyState === 'loading') {
|
|
155
|
+
document.addEventListener('DOMContentLoaded', addButton);
|
|
156
|
+
} else {
|
|
157
|
+
addButton();
|
|
158
|
+
}
|
|
159
|
+
})();
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _on_builder_inited(app: Sphinx) -> None:
|
|
164
|
+
if isinstance(app.builder, MarkdownBuilder):
|
|
165
|
+
app.config.exclude_patterns = [
|
|
166
|
+
*app.config.exclude_patterns,
|
|
167
|
+
*app.config.llm_friendly_exclude,
|
|
168
|
+
]
|
|
169
|
+
elif app.builder.format == "html":
|
|
170
|
+
app.add_js_file(None, body=_COPY_AS_MARKDOWN_BUTTON_JS)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _add_markdown_alternate_link(
|
|
174
|
+
app: Sphinx,
|
|
175
|
+
pagename: str,
|
|
176
|
+
templatename: str,
|
|
177
|
+
context: dict[str, Any],
|
|
178
|
+
doctree: nodes.document | None,
|
|
179
|
+
) -> None:
|
|
180
|
+
if (
|
|
181
|
+
app.builder.format != "html"
|
|
182
|
+
or pagename not in app.env.found_docs
|
|
183
|
+
or is_excluded(app, pagename)
|
|
184
|
+
):
|
|
185
|
+
return
|
|
186
|
+
html_uri = app.builder.get_target_uri(pagename)
|
|
187
|
+
md_uri = f"{posixpath.splitext(html_uri)[0]}.md"
|
|
188
|
+
href = relative_uri(html_uri, md_uri)
|
|
189
|
+
context["metatags"] = context.get("metatags", "") + (
|
|
190
|
+
f'\n<link rel="alternate" type="text/markdown" href="{href}">'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _on_build_finished(app: Sphinx, exception: Exception | None) -> None:
|
|
195
|
+
if exception is None and app.builder.name in {"html", "dirhtml"}:
|
|
196
|
+
write_llms_txt(app)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def setup(app: Sphinx) -> ExtensionMetadata:
|
|
200
|
+
app.add_builder(MarkdownBuilder)
|
|
201
|
+
app.add_builder(SingleFileMarkdownBuilder)
|
|
202
|
+
|
|
203
|
+
app.add_config_value("llm_friendly_exclude", [], "env", types=frozenset({list}))
|
|
204
|
+
app.add_config_value(
|
|
205
|
+
"llm_friendly_llms_txt_summary",
|
|
206
|
+
None,
|
|
207
|
+
"html",
|
|
208
|
+
types=frozenset({str, type(None)}),
|
|
209
|
+
)
|
|
210
|
+
app.add_config_value(
|
|
211
|
+
"llm_friendly_llms_txt_toctree_only", False, "html", types=frozenset({bool})
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
app.connect("builder-inited", _on_builder_inited)
|
|
215
|
+
app.connect("html-page-context", _add_markdown_alternate_link)
|
|
216
|
+
app.connect("build-finished", _on_build_finished)
|
|
217
|
+
app.connect("build-finished", remember_intersphinx_urls)
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"version": version("sphinx-llm-friendly"),
|
|
221
|
+
"parallel_read_safe": True,
|
|
222
|
+
"parallel_write_safe": True,
|
|
223
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import concurrent.futures
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from sphinx.cmd.build import main as sphinx_build_main
|
|
10
|
+
|
|
11
|
+
from ._intersphinx import rewrite_intersphinx_links
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from sphinx.application import Sphinx
|
|
15
|
+
|
|
16
|
+
_BUILDERS = ("html", "llm_markdown", "llm_singlemarkdown")
|
|
17
|
+
|
|
18
|
+
_intersphinx_urls: set[str] = set()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def remember_intersphinx_urls(app: Sphinx, exception: Exception | None) -> None:
|
|
22
|
+
"""Store the site URLs of ``intersphinx_mapping``, for :func:`_run_builder`
|
|
23
|
+
to report them to the parent process.
|
|
24
|
+
"""
|
|
25
|
+
mapping = getattr(app.config, "intersphinx_mapping", {})
|
|
26
|
+
_intersphinx_urls.clear()
|
|
27
|
+
_intersphinx_urls.update(uri for _name, (uri, _inventories) in mapping.values())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _run_builder(builder: str, source_dir: Path, build_dir: Path) -> set[str]:
|
|
31
|
+
exit_code = sphinx_build_main(
|
|
32
|
+
["-b", builder, str(source_dir), str(build_dir / builder)]
|
|
33
|
+
)
|
|
34
|
+
if exit_code != 0:
|
|
35
|
+
msg = f"sphinx builder failed for '{builder}' with exit code {exit_code}"
|
|
36
|
+
raise RuntimeError(msg)
|
|
37
|
+
return set(_intersphinx_urls)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build(source_dir: Path, build_dir: Path) -> Path:
|
|
41
|
+
"""Build the documentation in *source_dir* as HTML and Markdown, and
|
|
42
|
+
return the directory inside *build_dir* where the outputs are merged.
|
|
43
|
+
"""
|
|
44
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
intersphinx_urls: set[str] = set()
|
|
46
|
+
with concurrent.futures.ProcessPoolExecutor(max_workers=len(_BUILDERS)) as pool:
|
|
47
|
+
futures = [
|
|
48
|
+
pool.submit(_run_builder, builder, source_dir, build_dir)
|
|
49
|
+
for builder in _BUILDERS
|
|
50
|
+
]
|
|
51
|
+
for future in concurrent.futures.as_completed(futures):
|
|
52
|
+
intersphinx_urls |= future.result()
|
|
53
|
+
|
|
54
|
+
output_dir = build_dir / "all"
|
|
55
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
shutil.copytree(build_dir / "html", output_dir, dirs_exist_ok=True)
|
|
57
|
+
shutil.copytree(build_dir / "llm_markdown", output_dir, dirs_exist_ok=True)
|
|
58
|
+
shutil.copy2(
|
|
59
|
+
build_dir / "llm_singlemarkdown" / "index.md", output_dir / "llms-full.txt"
|
|
60
|
+
)
|
|
61
|
+
rewrite_intersphinx_links(
|
|
62
|
+
[
|
|
63
|
+
*sorted(output_dir.rglob("*.md")),
|
|
64
|
+
output_dir / "llms.txt",
|
|
65
|
+
output_dir / "llms-full.txt",
|
|
66
|
+
],
|
|
67
|
+
intersphinx_urls,
|
|
68
|
+
)
|
|
69
|
+
return output_dir
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: list[str] | None = None) -> int:
|
|
73
|
+
parser = argparse.ArgumentParser(prog="sphinx-llm-friendly")
|
|
74
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
75
|
+
build_parser = subparsers.add_parser(
|
|
76
|
+
"build", help="build HTML and Markdown documentation"
|
|
77
|
+
)
|
|
78
|
+
build_parser.add_argument("source_dir", type=Path)
|
|
79
|
+
build_parser.add_argument("build_dir", type=Path)
|
|
80
|
+
args = parser.parse_args(argv)
|
|
81
|
+
try:
|
|
82
|
+
output_dir = build(args.source_dir, args.build_dir)
|
|
83
|
+
except RuntimeError as error:
|
|
84
|
+
parser.exit(1, f"{error}\n")
|
|
85
|
+
print(f"\nDocumentation generated in {output_dir}.")
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
raise SystemExit(main())
|