liferay-context-builder 0.6.2__py3-none-any.whl
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.
- liferay_context_builder-0.6.2.dist-info/METADATA +354 -0
- liferay_context_builder-0.6.2.dist-info/RECORD +12 -0
- liferay_context_builder-0.6.2.dist-info/WHEEL +4 -0
- liferay_context_builder-0.6.2.dist-info/entry_points.txt +4 -0
- liferay_context_builder-0.6.2.dist-info/licenses/LICENSE +21 -0
- liferay_docs_scraper/__init__.py +0 -0
- liferay_docs_scraper/classify_pages.py +41 -0
- liferay_docs_scraper/community.py +440 -0
- liferay_docs_scraper/doctor.py +199 -0
- liferay_docs_scraper/filter_urls.py +207 -0
- liferay_docs_scraper/index.py +201 -0
- liferay_docs_scraper/pipeline.py +571 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: liferay-context-builder
|
|
3
|
+
Version: 0.6.2
|
|
4
|
+
Summary: Build a local, cited Liferay DXP context library for the liferay-expert Claude Code skill.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: <3.14,>=3.10
|
|
8
|
+
Requires-Dist: beautifulsoup4<5,>=4.12
|
|
9
|
+
Requires-Dist: crawl4ai<1.0,>=0.9.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# liferay-context-builder
|
|
13
|
+
|
|
14
|
+
Give Claude Code a local, source-backed Liferay DXP knowledge base it can
|
|
15
|
+
actually read before answering.
|
|
16
|
+
|
|
17
|
+
`liferay-context-builder` turns the public Liferay docs into a local context
|
|
18
|
+
library and pairs it with the `liferay-expert` Claude Code skill. The result is
|
|
19
|
+
simple: when someone on the team asks a Liferay question, the assistant can
|
|
20
|
+
look up the relevant docs, cite the original URL, and avoid guessing from model
|
|
21
|
+
memory.
|
|
22
|
+
|
|
23
|
+
It is built for team use:
|
|
24
|
+
|
|
25
|
+
- Answers stay tied to official `learn.liferay.com` sources.
|
|
26
|
+
- Every project can share the same local docs folder.
|
|
27
|
+
- There is no bundled Liferay content, vector database, or embedding service to
|
|
28
|
+
manage.
|
|
29
|
+
- A doctor command checks whether the docs and skill are ready.
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
[Download the MP4 demo](docs/assets/liferay-doc-demo.mp4)
|
|
34
|
+
|
|
35
|
+
[Project page](https://mordonez.github.io/liferay-context-builder/) ·
|
|
36
|
+
[PyPI package](https://pypi.org/project/liferay-context-builder/) · Python
|
|
37
|
+
3.10-3.13 · [MIT license](LICENSE)
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
From zero to source-backed Liferay answers in Claude Code:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# 1. One-time browser setup for crawl4ai/Playwright
|
|
45
|
+
uvx --from crawl4ai crawl4ai-setup
|
|
46
|
+
|
|
47
|
+
# 2. Build the local Liferay DXP context library in ~/.liferay-docs
|
|
48
|
+
uvx liferay-context-builder
|
|
49
|
+
|
|
50
|
+
# 3. Install the Claude Code skill in your current project
|
|
51
|
+
npx skills add mordonez/liferay-context-builder --skill liferay-expert -a claude-code
|
|
52
|
+
|
|
53
|
+
# 4. Verify docs freshness and skill installation
|
|
54
|
+
uvx --from liferay-context-builder liferay-context-builder-doctor
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then ask Claude Code something like:
|
|
58
|
+
|
|
59
|
+
> How do I configure synonym sets in Liferay Search?
|
|
60
|
+
|
|
61
|
+
The skill searches the local context library, reads the best matching pages,
|
|
62
|
+
and cites the original `learn.liferay.com` URL.
|
|
63
|
+
|
|
64
|
+
Keep `-a claude-code` in the install command. It avoids interactive installer
|
|
65
|
+
edge cases where the skill can appear installed but not land in
|
|
66
|
+
`.claude/skills/`.
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
- Python 3.10-3.13
|
|
71
|
+
- [`uv`](https://docs.astral.sh/uv/)
|
|
72
|
+
- Node/npm for `npx skills add`
|
|
73
|
+
|
|
74
|
+
`crawl4ai` uses Playwright. Run the browser setup once per machine before the
|
|
75
|
+
first scrape:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uvx --from crawl4ai crawl4ai-setup
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## How It Works
|
|
82
|
+
|
|
83
|
+
```mermaid
|
|
84
|
+
flowchart LR
|
|
85
|
+
A[learn.liferay.com] --> B[crawl4ai BFS crawl]
|
|
86
|
+
B --> C[local Markdown in ~/.liferay-docs]
|
|
87
|
+
C --> D[search_index.jsonl and anomalies.jsonl]
|
|
88
|
+
C --> E[liferay-expert Claude Code skill]
|
|
89
|
+
D --> E
|
|
90
|
+
E --> F[cited Liferay answers]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The official context builder starts at
|
|
94
|
+
`https://learn.liferay.com/w/dxp/index` and uses crawl4ai's BFS deep crawler to
|
|
95
|
+
follow internal `/w/dxp/*` links. For each page, it extracts the article body,
|
|
96
|
+
classifies the URL into a Liferay capability, and writes Markdown locally.
|
|
97
|
+
|
|
98
|
+
The builder is intentionally boring:
|
|
99
|
+
|
|
100
|
+
- It fetches from the live Liferay docs when you run it; this package does not
|
|
101
|
+
redistribute Liferay documentation text.
|
|
102
|
+
- It writes to one shared docs directory, so every project can use the same
|
|
103
|
+
corpus.
|
|
104
|
+
- It retries through crawl4ai, writes files atomically, and exits non-zero when
|
|
105
|
+
the crawl or page fetches fail.
|
|
106
|
+
- It never starts a long scrape from inside the skill. If docs are missing, the
|
|
107
|
+
skill tells you which command to run.
|
|
108
|
+
|
|
109
|
+
## Where Files Go
|
|
110
|
+
|
|
111
|
+
By default, everything is written under:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
~/.liferay-docs
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use `LIFERAY_DOCS_DIR` when you want a repo-local or custom corpus:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
export LIFERAY_DOCS_DIR="$PWD/.liferay-docs"
|
|
121
|
+
uvx liferay-context-builder
|
|
122
|
+
uvx --from liferay-context-builder liferay-context-builder-doctor
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Layout:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
~/.liferay-docs/
|
|
129
|
+
raw/{capability}/*.md
|
|
130
|
+
raw/_navigation/{capability}/*.md
|
|
131
|
+
raw/_removed/{capability}/*.md
|
|
132
|
+
raw/community-howto/{capability}/*.md
|
|
133
|
+
raw/community-troubleshooting/{capability}/*.md
|
|
134
|
+
reports/filtered/
|
|
135
|
+
search_index.jsonl
|
|
136
|
+
anomalies.jsonl
|
|
137
|
+
summary.json
|
|
138
|
+
*_urls.txt
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`raw/{capability}/*.md` is the main official-docs corpus the skill reads first.
|
|
142
|
+
`raw/_navigation/` keeps table-of-contents/navigation pages out of normal
|
|
143
|
+
answers while preserving them. `raw/_removed/` holds pages only after the
|
|
144
|
+
scraper directly confirms their original URL is gone.
|
|
145
|
+
|
|
146
|
+
## Refreshing Official Docs
|
|
147
|
+
|
|
148
|
+
Run the scraper again whenever you want fresh docs:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
uvx liferay-context-builder
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
A normal full run usually takes tens of minutes. For a smoke test:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
uvx liferay-context-builder --max-pages 200
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Useful options:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
uvx liferay-context-builder --max-depth 12
|
|
164
|
+
uvx liferay-context-builder --max-pages 3000
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Each full run starts from the current site state. If a previously known page is
|
|
168
|
+
not rediscovered by BFS, the scraper checks that page directly before moving it
|
|
169
|
+
to `raw/_removed/`. If the page is still alive, it refreshes it directly and
|
|
170
|
+
records the BFS coverage gap in the reports.
|
|
171
|
+
|
|
172
|
+
## Community Articles
|
|
173
|
+
|
|
174
|
+
Community articles are optional, larger, and lower-authority than the official
|
|
175
|
+
DXP docs:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
uvx --from liferay-context-builder liferay-context-builder-community
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
This fetches Liferay community How-To and Troubleshooting articles from
|
|
182
|
+
`learn.liferay.com/kb-article/*`. They are stored separately:
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
raw/community-howto/{capability}/*.md
|
|
186
|
+
raw/community-troubleshooting/{capability}/*.md
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Many community articles have no usable capability tag, so they go to
|
|
190
|
+
`_uncategorized/`. The skill treats community content as secondary evidence and
|
|
191
|
+
says so when citing it.
|
|
192
|
+
|
|
193
|
+
Useful commands:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Only How-To articles
|
|
197
|
+
uvx --from liferay-context-builder liferay-context-builder-community --resource-type howto
|
|
198
|
+
|
|
199
|
+
# Smaller test run per resource type
|
|
200
|
+
uvx --from liferay-context-builder liferay-context-builder-community --limit 100
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Community scraping can take much longer than the official-docs scrape because
|
|
204
|
+
it fetches thousands of additional articles.
|
|
205
|
+
|
|
206
|
+
## Installing The Skill
|
|
207
|
+
|
|
208
|
+
Install `liferay-expert` into each Claude Code project where you want Liferay
|
|
209
|
+
help:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
npx skills add mordonez/liferay-context-builder --skill liferay-expert -a claude-code
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Manual install also works: place the skill file at:
|
|
216
|
+
|
|
217
|
+
```text
|
|
218
|
+
.claude/skills/liferay-expert/SKILL.md
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The skill resolves docs the same way the scraper does:
|
|
222
|
+
|
|
223
|
+
1. `$LIFERAY_DOCS_DIR`, if set.
|
|
224
|
+
2. `~/.liferay-docs`, otherwise.
|
|
225
|
+
|
|
226
|
+
When answering, it searches `reports/filtered/search_index.jsonl` when present,
|
|
227
|
+
falls back to normal file search under `raw/`, reads Markdown files directly,
|
|
228
|
+
and cites the `url:` frontmatter. Official docs are preferred over community
|
|
229
|
+
articles when both cover the same topic.
|
|
230
|
+
|
|
231
|
+
## Doctor
|
|
232
|
+
|
|
233
|
+
Use the doctor when something feels off:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
uvx --from liferay-context-builder liferay-context-builder-doctor
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
It checks:
|
|
240
|
+
|
|
241
|
+
- Which docs directory is active.
|
|
242
|
+
- Whether official Markdown exists.
|
|
243
|
+
- How many community Markdown files exist.
|
|
244
|
+
- The official-docs freshness window.
|
|
245
|
+
- Search index and anomaly report entry counts.
|
|
246
|
+
- Whether `.claude/skills/liferay-expert/SKILL.md` exists in the current
|
|
247
|
+
project.
|
|
248
|
+
|
|
249
|
+
To inspect a different project directory:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
uvx --from liferay-context-builder liferay-context-builder-doctor --project-dir /path/to/project
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The doctor does not scrape docs and does not install the skill. It only reports
|
|
256
|
+
status and prints the next command to run.
|
|
257
|
+
|
|
258
|
+
## Reports
|
|
259
|
+
|
|
260
|
+
The scraper writes agent-facing reports under `reports/filtered/`.
|
|
261
|
+
|
|
262
|
+
`search_index.jsonl` is a local retrieval index. Each JSON line includes title,
|
|
263
|
+
source URL, source type, capability, file path, headings, and `fetched_at`. The
|
|
264
|
+
skill uses it first because it is faster and cleaner than searching every
|
|
265
|
+
Markdown file.
|
|
266
|
+
|
|
267
|
+
`anomalies.jsonl` is an informational scrape-quality report. It flags signals
|
|
268
|
+
like very short bodies, missing titles, known error markers, unusually large
|
|
269
|
+
pages, and large body-size swings versus the previous local copy. It does not
|
|
270
|
+
mean a page is unusable; it means the page may deserve a quick check before you
|
|
271
|
+
trust or cite it heavily.
|
|
272
|
+
|
|
273
|
+
`summary.json` records the latest run counts, crawl failures, direct refreshes,
|
|
274
|
+
coverage gaps, and search index size.
|
|
275
|
+
|
|
276
|
+
## Troubleshooting
|
|
277
|
+
|
|
278
|
+
**`crawl4ai` or browser errors on the first run**
|
|
279
|
+
|
|
280
|
+
Run the Playwright setup again:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
uvx --from crawl4ai crawl4ai-setup
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Claude Code says the skill is missing**
|
|
287
|
+
|
|
288
|
+
Run the install command from the project where you are using Claude Code:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
npx skills add mordonez/liferay-context-builder --skill liferay-expert -a claude-code
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Then verify:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
uvx --from liferay-context-builder liferay-context-builder-doctor
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Claude Code says docs are missing**
|
|
301
|
+
|
|
302
|
+
Check whether you are using a custom docs directory:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
echo "$LIFERAY_DOCS_DIR"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
If it is empty, the skill expects `~/.liferay-docs`. If it points somewhere
|
|
309
|
+
else, run the scraper with that same environment variable.
|
|
310
|
+
|
|
311
|
+
**Docs are stale**
|
|
312
|
+
|
|
313
|
+
Refresh official docs:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
uvx liferay-context-builder
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The doctor warns when official docs are older than about seven days.
|
|
320
|
+
|
|
321
|
+
**A scrape stops partway through**
|
|
322
|
+
|
|
323
|
+
Rerun the same command. Already written Markdown remains usable, but a failed
|
|
324
|
+
run exits non-zero and avoids treating untouched pages as removed.
|
|
325
|
+
|
|
326
|
+
**Community answers feel weaker than official docs**
|
|
327
|
+
|
|
328
|
+
That is expected. Community How-To and Troubleshooting articles are useful for
|
|
329
|
+
practical cases and errors, but the skill should label them as community
|
|
330
|
+
content and prefer official docs when official docs answer the question.
|
|
331
|
+
|
|
332
|
+
## Development
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
uv sync --group dev
|
|
336
|
+
uv run ruff check .
|
|
337
|
+
uv run --with pytest python -m pytest
|
|
338
|
+
uv build
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Run `uv sync --group dev` once before local development so the project and dev
|
|
342
|
+
tools are installed into uv's project environment. The pytest command uses
|
|
343
|
+
`python -m pytest` with `--with pytest` because older or unsynced uv
|
|
344
|
+
environments can fail to find the `pytest` console script even when Python can
|
|
345
|
+
run the module.
|
|
346
|
+
|
|
347
|
+
CI runs lint, tests, and package build on Python 3.10, 3.11, 3.12, and 3.13.
|
|
348
|
+
It does not run a real scrape. Release publishing is documented in
|
|
349
|
+
[`docs/release.md`](docs/release.md).
|
|
350
|
+
|
|
351
|
+
## License
|
|
352
|
+
|
|
353
|
+
[MIT](LICENSE) applies to this tool and skill only. Liferay documentation
|
|
354
|
+
content remains Liferay's content and is fetched locally by each user.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
liferay_docs_scraper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
liferay_docs_scraper/classify_pages.py,sha256=yXo6eKLlsFpoM1CKzcZUNEWBN8RcQ1Nc_2qNgqnkLLs,1448
|
|
3
|
+
liferay_docs_scraper/community.py,sha256=I0p6iZz-H66uwo4CPagnqiYbBi0LeexClNMoFdN1fo4,18860
|
|
4
|
+
liferay_docs_scraper/doctor.py,sha256=A3f8jttJ-bw3a_JCouXpJyTp2pxLqEN17XLrcmpTNXY,6859
|
|
5
|
+
liferay_docs_scraper/filter_urls.py,sha256=3qZCcGghJrjnfLNflj5OwwHzsKyKqmPBypVC0KrxaRQ,7403
|
|
6
|
+
liferay_docs_scraper/index.py,sha256=xpdAewS-Edxe90_S0XJGGZT983LzAXVZNWnmxMTcjdQ,6442
|
|
7
|
+
liferay_docs_scraper/pipeline.py,sha256=-JQZBKCmiQl6L62G9U3uPguZZAFiCdAqSTW_HGB2Lkk,24486
|
|
8
|
+
liferay_context_builder-0.6.2.dist-info/METADATA,sha256=LmhbeWEHg6EH0lI83aEaxoDI_Eu-kQQXYXCyErg6OSk,10507
|
|
9
|
+
liferay_context_builder-0.6.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
10
|
+
liferay_context_builder-0.6.2.dist-info/entry_points.txt,sha256=X5Cc5NdbdtYY4gdrunOYDupeQMZd-OAZYjrIa_bAC48,217
|
|
11
|
+
liferay_context_builder-0.6.2.dist-info/licenses/LICENSE,sha256=li7C8a1CCl-Qhn8tmQ55RDdRnOziMgqa08pfVjJuSyw,1072
|
|
12
|
+
liferay_context_builder-0.6.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Miguel Ordoñez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Navigation-vs-content heuristic, shared with crawl4ai_pipeline.py.
|
|
3
|
+
|
|
4
|
+
An "index"/navigation page is one whose body (frontmatter stripped) is
|
|
5
|
+
short and consists mostly of links to subpages -- no substantial technical
|
|
6
|
+
content of its own. Everything else is "content".
|
|
7
|
+
|
|
8
|
+
Heuristic (no API calls, pure text analysis):
|
|
9
|
+
- Strip Markdown link syntax down to visible text (`[text](url)` -> `text`)
|
|
10
|
+
and strip heading/emphasis markup (`#`, `*`, `_`, backticks).
|
|
11
|
+
- total_words: word count of that visible text.
|
|
12
|
+
- link_ratio: fraction of those words that come from inside a Markdown
|
|
13
|
+
link's link-text span.
|
|
14
|
+
- "index" iff total_words < INDEX_MAX_WORDS and link_ratio >= INDEX_MIN_LINK_RATIO.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
INDEX_MAX_WORDS = 150
|
|
20
|
+
INDEX_MIN_LINK_RATIO = 0.5
|
|
21
|
+
|
|
22
|
+
LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)]*)\)")
|
|
23
|
+
MARKUP_RE = re.compile(r"[#*_`\\]")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def analyze_body(body: str) -> tuple[int, float]:
|
|
27
|
+
links = LINK_RE.findall(body)
|
|
28
|
+
link_word_count = sum(len(text.split()) for text, _url in links)
|
|
29
|
+
|
|
30
|
+
visible = LINK_RE.sub(lambda m: m.group(1), body)
|
|
31
|
+
visible = MARKUP_RE.sub("", visible)
|
|
32
|
+
total_words = len(visible.split())
|
|
33
|
+
|
|
34
|
+
link_ratio = (link_word_count / total_words) if total_words else 0.0
|
|
35
|
+
return total_words, link_ratio
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def classify(total_words: int, link_ratio: float) -> str:
|
|
39
|
+
if total_words < INDEX_MAX_WORDS and link_ratio >= INDEX_MIN_LINK_RATIO:
|
|
40
|
+
return "index"
|
|
41
|
+
return "content"
|