domdown 0.1.1__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.
Files changed (54) hide show
  1. domdown/__init__.py +16 -0
  2. domdown/_constants/__init__.py +33 -0
  3. domdown/_constants/selectors.py +305 -0
  4. domdown/_core/__init__.py +13 -0
  5. domdown/_core/context.py +22 -0
  6. domdown/_core/metadata.py +20 -0
  7. domdown/_core/options.py +23 -0
  8. domdown/_core/result.py +17 -0
  9. domdown/_document/__init__.py +7 -0
  10. domdown/_document/clean.py +549 -0
  11. domdown/_document/parse.py +9 -0
  12. domdown/_document/select.py +485 -0
  13. domdown/_frontmatter/__init__.py +6 -0
  14. domdown/_frontmatter/compose.py +10 -0
  15. domdown/_frontmatter/serialize.py +39 -0
  16. domdown/_metadata/__init__.py +5 -0
  17. domdown/_metadata/extract.py +97 -0
  18. domdown/_metadata/helpers.py +180 -0
  19. domdown/_metadata/selectors.py +79 -0
  20. domdown/_pipeline/__init__.py +5 -0
  21. domdown/_pipeline/runner.py +63 -0
  22. domdown/_text/__init__.py +16 -0
  23. domdown/_text/frontmatter.py +30 -0
  24. domdown/_text/normalize.py +20 -0
  25. domdown/_text/url.py +43 -0
  26. domdown/adapters/__init__.py +7 -0
  27. domdown/adapters/base.py +31 -0
  28. domdown/adapters/github.py +229 -0
  29. domdown/adapters/registry.py +46 -0
  30. domdown/api.py +15 -0
  31. domdown/markdown/__init__.py +17 -0
  32. domdown/markdown/block.py +349 -0
  33. domdown/markdown/code.py +54 -0
  34. domdown/markdown/images.py +64 -0
  35. domdown/markdown/inline.py +81 -0
  36. domdown/markdown/links.py +42 -0
  37. domdown/markdown/lists.py +40 -0
  38. domdown/markdown/postprocess.py +43 -0
  39. domdown/markdown/tables.py +52 -0
  40. domdown/stages/__init__.py +21 -0
  41. domdown/stages/base.py +18 -0
  42. domdown/stages/clean.py +57 -0
  43. domdown/stages/frontmatter.py +25 -0
  44. domdown/stages/markdown.py +23 -0
  45. domdown/stages/metadata.py +21 -0
  46. domdown/stages/parse.py +21 -0
  47. domdown/stages/postprocess.py +21 -0
  48. domdown/stages/preserve.py +67 -0
  49. domdown-0.1.1.dist-info/METADATA +316 -0
  50. domdown-0.1.1.dist-info/RECORD +54 -0
  51. domdown-0.1.1.dist-info/WHEEL +5 -0
  52. domdown-0.1.1.dist-info/entry_points.txt +2 -0
  53. domdown-0.1.1.dist-info/licenses/AUTHORS.md +10 -0
  54. domdown-0.1.1.dist-info/top_level.txt +1 -0
domdown/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from .api import html_to_markdown
4
+ from ._core import DomdownOptions, HtmlMetadata, HtmlToMarkdownResult
5
+ from ._pipeline import HtmlToMarkdownPipeline
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ __all__ = [
10
+ "__version__",
11
+ "DomdownOptions",
12
+ "HtmlMetadata",
13
+ "HtmlToMarkdownResult",
14
+ "HtmlToMarkdownPipeline",
15
+ "html_to_markdown",
16
+ ]
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from .selectors import (
4
+ BOILERPLATE_PHRASES,
5
+ CONTENT_SELECTORS,
6
+ CONTENT_SELECTORS_EXACT,
7
+ CONTENT_SELECTORS_FALLBACK,
8
+ DEFAULT_REMOVE_SELECTORS,
9
+ JS_SHELL_PHRASES,
10
+ HEADER_MARKERS,
11
+ NOISE_MARKERS,
12
+ RELATED_PHRASES,
13
+ REFINABLE_CHILD_TAGS,
14
+ ROOT_SELECTORS,
15
+ SHARE_SELECTORS,
16
+ SKIP_TAGS,
17
+ )
18
+
19
+ __all__ = [
20
+ "BOILERPLATE_PHRASES",
21
+ "CONTENT_SELECTORS",
22
+ "CONTENT_SELECTORS_EXACT",
23
+ "CONTENT_SELECTORS_FALLBACK",
24
+ "DEFAULT_REMOVE_SELECTORS",
25
+ "JS_SHELL_PHRASES",
26
+ "HEADER_MARKERS",
27
+ "NOISE_MARKERS",
28
+ "RELATED_PHRASES",
29
+ "REFINABLE_CHILD_TAGS",
30
+ "ROOT_SELECTORS",
31
+ "SHARE_SELECTORS",
32
+ "SKIP_TAGS",
33
+ ]
@@ -0,0 +1,305 @@
1
+ from __future__ import annotations
2
+
3
+ # Tags that should be dropped entirely during cleanup and rendering.
4
+ SKIP_TAGS = {
5
+ "aside",
6
+ "button",
7
+ "form",
8
+ "iframe",
9
+ "input",
10
+ "link",
11
+ "meta",
12
+ "noscript",
13
+ "script",
14
+ "select",
15
+ "style",
16
+ "svg",
17
+ "textarea",
18
+ }
19
+
20
+ # Selectors that are typically noise in article-like pages.
21
+ SHARE_SELECTORS = (
22
+ ".share-widget",
23
+ ".heateor_sss_sharing_container",
24
+ ".heateor_sss_more",
25
+ ".more-link",
26
+ "[class*='share']",
27
+ "[id*='share']",
28
+ "[class*='social-sharing']",
29
+ "[id*='social-sharing']",
30
+ "[class*='social-links']",
31
+ "[id*='social-links']",
32
+ "[class*='socials']",
33
+ "[id*='socials']",
34
+ )
35
+
36
+ # Direct children that are still broad enough to be considered content shells
37
+ # during subtree refinement.
38
+ REFINABLE_CHILD_TAGS = (
39
+ "article",
40
+ "aside",
41
+ "blockquote",
42
+ "div",
43
+ "figure",
44
+ "main",
45
+ "ol",
46
+ "section",
47
+ "table",
48
+ "ul",
49
+ )
50
+
51
+ DEFAULT_REMOVE_SELECTORS = (
52
+ ".ad-fixed__wrapper",
53
+ ".post-hero__ad",
54
+ ".post-hero__content",
55
+ ".post-hero__image",
56
+ ".ad-leaderboard",
57
+ ".ad__disclaimer",
58
+ ".ad__title",
59
+ ".ad__upsell",
60
+ ".float-share",
61
+ ".mobile-share",
62
+ "a[href='#main']",
63
+ "a[href='#content']",
64
+ ".post-head",
65
+ ".post-header",
66
+ ".post-badge",
67
+ ".post-access-cta",
68
+ ".story-tools",
69
+ ".text-settings",
70
+ ".text-settings-menu",
71
+ ".text-settings-dropdown-story",
72
+ ".text-settings-dropdown-nav",
73
+ ".comments-wrapper",
74
+ ".comments-picks-list",
75
+ ".staff-picks-title",
76
+ ".wp-forum-connect-comments",
77
+ ".xf_thread_iframe_wrapper",
78
+ ".search-container",
79
+ "#search-result-wrapper",
80
+ ".searchresults",
81
+ ".noresults",
82
+ ".comment-pick",
83
+ ".single-most-read",
84
+ ".component-most-read",
85
+ ".news-form",
86
+ ".article-info",
87
+ ".article_util",
88
+ ".container_postbtn",
89
+ ".another_category",
90
+ ".company-box",
91
+ ".box_comment",
92
+ ".box_related_article",
93
+ ".sidebar",
94
+ "#sidebar",
95
+ ".et_pb_title_meta_container",
96
+ ".et_pb_text_0",
97
+ ".tag-list",
98
+ ".dsm_open_icon",
99
+ ".dsm_close_icon",
100
+ ".dsm-faq-item-open_icon",
101
+ ".dsm-faq-item-close_icon",
102
+ ".sharebelow",
103
+ ".schema_org",
104
+ ".tags",
105
+ ".article-categories",
106
+ "[class*='article-categories']",
107
+ ".story-title",
108
+ ".postmeta",
109
+ "[class*='follow']",
110
+ "[class*='sponsored']",
111
+ "[rel*='sponsored']",
112
+ "[class*='note-b']",
113
+ "[class*='dog_two']",
114
+ ) + SHARE_SELECTORS
115
+
116
+ # Selectors that usually point at the content subtree inside a larger page shell.
117
+ # Exact selectors are preferred first because substring selectors can match chrome.
118
+ CONTENT_SELECTORS_EXACT = (
119
+ ".articlebody",
120
+ ".post-body",
121
+ ".entry-content",
122
+ ".content",
123
+ ".article_cont",
124
+ ".tt_article_useless_p_margin",
125
+ ".page--body",
126
+ "#content",
127
+ ".content-body",
128
+ ".post-content",
129
+ ".post__body",
130
+ ".post__content",
131
+ ".s-blog-post__body",
132
+ ".BodyText__content",
133
+ ".hb-content__text",
134
+ ".markdown-body",
135
+ ".story-body",
136
+ ".story-shell",
137
+ )
138
+
139
+ # Broad selectors are only used as a fallback when exact selectors do not find
140
+ # a confident content subtree.
141
+ CONTENT_SELECTORS_FALLBACK = (
142
+ "[class*='body']",
143
+ "[id*='body']",
144
+ "[class*='entry']",
145
+ "[id*='entry']",
146
+ "[class*='content']",
147
+ "[id*='content']",
148
+ )
149
+
150
+ CONTENT_SELECTORS = CONTENT_SELECTORS_EXACT + CONTENT_SELECTORS_FALLBACK
151
+
152
+ # Root selectors used to choose the most relevant content shell before cleanup.
153
+ ROOT_SELECTORS = (
154
+ "article",
155
+ "main",
156
+ "main.page--body",
157
+ "[role='article']",
158
+ "[role='main']",
159
+ ".post-body",
160
+ ".articlebody",
161
+ ".entry-content",
162
+ ".content",
163
+ ".article_cont",
164
+ ".tt_article_useless_p_margin",
165
+ ".page--body",
166
+ "#content",
167
+ ".content-body",
168
+ ".post-content",
169
+ ".post__body",
170
+ ".post__content",
171
+ ".s-blog-post__body",
172
+ ".BodyText__content",
173
+ ".hb-content__text",
174
+ ".markdown-body",
175
+ ".story-body",
176
+ ".story-shell",
177
+ "[class*='body']",
178
+ "[id*='body']",
179
+ "[class*='entry']",
180
+ "[id*='entry']",
181
+ "[class*='content']",
182
+ "[id*='content']",
183
+ "body",
184
+ )
185
+
186
+ # Class or id markers that usually indicate page chrome, ads, or engagement widgets.
187
+ NOISE_MARKERS = (
188
+ "ai-links",
189
+ "announcement",
190
+ "banner",
191
+ "brand",
192
+ "author",
193
+ "bio",
194
+ "cookie",
195
+ "share",
196
+ "social",
197
+ "breadcrumb",
198
+ "lead",
199
+ "deck",
200
+ "standfirst",
201
+ "teaser",
202
+ "excerpt",
203
+ "related",
204
+ "recommend",
205
+ "feedback",
206
+ "newsletter",
207
+ "subscribe",
208
+ "promo",
209
+ "toolbar",
210
+ "pagination",
211
+ "pager",
212
+ "toc",
213
+ "table-of-contents",
214
+ "debug",
215
+ "cta",
216
+ "nav",
217
+ "footer",
218
+ "tags",
219
+ "postmeta",
220
+ "story-title",
221
+ "post-head",
222
+ "navigation",
223
+ "cookie",
224
+ "consent",
225
+ "popup",
226
+ "modal",
227
+ "advert",
228
+ "sponsor",
229
+ "logo",
230
+ "masthead",
231
+ "notice",
232
+ "overlay",
233
+ "follow",
234
+ "sponsored",
235
+ "note-b",
236
+ "dog_two",
237
+ # Additional markers from defuddle
238
+ "read-more",
239
+ "article-card",
240
+ "post-meta",
241
+ "article-actions",
242
+ "post-actions",
243
+ "byline",
244
+ "author-block",
245
+ "meta-info",
246
+ "sidebar",
247
+ "aside",
248
+ "related-posts",
249
+ "related-content",
250
+ "promo-box",
251
+ "sponsor-box",
252
+ "ad-box",
253
+ "promo-block",
254
+ )
255
+
256
+ # Structural chrome markers for top-of-article wrappers that repeat metadata.
257
+ HEADER_MARKERS = (
258
+ "byline",
259
+ "author",
260
+ "date",
261
+ "time",
262
+ "heading--sub",
263
+ "title-sub",
264
+ "teaser",
265
+ "kicker",
266
+ "standfirst",
267
+ "deck",
268
+ )
269
+
270
+ # Phrases commonly used to label related-link blocks.
271
+ RELATED_PHRASES = (
272
+ "related categories",
273
+ "related topics",
274
+ "related articles",
275
+ "related posts",
276
+ "recommended",
277
+ "latest news",
278
+ "you may also like",
279
+ "more from",
280
+ "most read",
281
+ )
282
+
283
+ # Common boilerplate phrases that appear in documentation and feedback shells.
284
+ BOILERPLATE_PHRASES = (
285
+ "thanks for letting us know this page needs work",
286
+ "we're sorry we let you down",
287
+ "help improve",
288
+ "learn how to contribute",
289
+ "view this page on github",
290
+ "report a problem with this content",
291
+ "if you've got a moment",
292
+ "how we can make the documentation better",
293
+ "this post is for paid members only",
294
+ "no results found",
295
+ "developing story",
296
+ "stay tuned for updates",
297
+ )
298
+
299
+ # Placeholder phrases used by portal-style shells that require JavaScript
300
+ # before the real body is rendered.
301
+ JS_SHELL_PHRASES = (
302
+ "this app needs javascript to run",
303
+ "please enable javascript in your browser and try again",
304
+ "javascript is required",
305
+ )
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from .context import PipelineContext
4
+ from .metadata import HtmlMetadata
5
+ from .options import DomdownOptions
6
+ from .result import HtmlToMarkdownResult
7
+
8
+ __all__ = [
9
+ "DomdownOptions",
10
+ "HtmlMetadata",
11
+ "HtmlToMarkdownResult",
12
+ "PipelineContext",
13
+ ]
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from .metadata import HtmlMetadata
7
+ from .options import DomdownOptions
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class PipelineContext:
12
+ """Mutable state passed between pipeline stages."""
13
+
14
+ html: str
15
+ options: DomdownOptions
16
+ document: Any | None = None
17
+ cleaned_html: str | None = None
18
+ markdown: str = ""
19
+ metadata: HtmlMetadata | None = None
20
+ frontmatter: str | None = None
21
+ rendered_document: str | None = None
22
+ warnings: list[str] = field(default_factory=list)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class HtmlMetadata:
8
+ """Normalized article metadata extracted from the source HTML."""
9
+
10
+ title: str | None = None
11
+ site_name: str | None = None
12
+ source: str | None = None
13
+ author: tuple[str, ...] = ()
14
+ published: str | None = None
15
+ created: str | None = None
16
+ description: str | None = None
17
+ tags: tuple[str, ...] = ()
18
+ language: str | None = None
19
+ canonical_url: str | None = None
20
+ image: str | None = None
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class DomdownOptions:
8
+ """Configuration for HTML parsing, cleanup, and output shaping."""
9
+
10
+ base_url: str | None = None
11
+ created: str | None = None
12
+ extract_metadata: bool = True
13
+ emit_frontmatter: bool = True
14
+ prefer_article_body: bool = True
15
+ author_priority: str = "visible"
16
+ frontmatter_tags: tuple[str, ...] = ()
17
+ preserve_images: bool = True
18
+ preserve_tables: bool = True
19
+ preserve_code_blocks: bool = True
20
+ strip_hidden: bool = True
21
+ remove_selectors: tuple[str, ...] = ()
22
+ keep_selectors: tuple[str, ...] = ()
23
+ unwrap_selectors: tuple[str, ...] = ()
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .metadata import HtmlMetadata
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class HtmlToMarkdownResult:
10
+ """Final output produced by the pipeline."""
11
+
12
+ markdown: str
13
+ cleaned_html: str | None = None
14
+ metadata: HtmlMetadata | None = None
15
+ frontmatter: str | None = None
16
+ document: str | None = None
17
+ warnings: tuple[str, ...] = ()
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from .clean import clean_root
4
+ from .parse import parse_html
5
+ from .select import choose_root
6
+
7
+ __all__ = ["choose_root", "clean_root", "parse_html"]