wordpress-markdown-blog-loader 1.6.3__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.
- wordpress_markdown_blog_loader/__init__.py +0 -0
- wordpress_markdown_blog_loader/__main__.py +32 -0
- wordpress_markdown_blog_loader/api.py +658 -0
- wordpress_markdown_blog_loader/blog.py +621 -0
- wordpress_markdown_blog_loader/check_links.py +77 -0
- wordpress_markdown_blog_loader/download.py +67 -0
- wordpress_markdown_blog_loader/name_to_email.py +22 -0
- wordpress_markdown_blog_loader/new.py +158 -0
- wordpress_markdown_blog_loader/remove_newlines.py +22 -0
- wordpress_markdown_blog_loader/upload.py +112 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/METADATA +219 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/RECORD +16 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/WHEEL +5 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/entry_points.txt +2 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/licenses/LICENSE +202 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Union
|
|
7
|
+
from urllib.parse import urlparse, ParseResult
|
|
8
|
+
|
|
9
|
+
import bs4
|
|
10
|
+
import frontmatter
|
|
11
|
+
import markdownify
|
|
12
|
+
import pytz
|
|
13
|
+
from PIL import Image
|
|
14
|
+
from binx_og_image_generator import generate as generate_og_image
|
|
15
|
+
from binx_og_image_generator.generator import Blog as ImageGeneratorBlog
|
|
16
|
+
from markdown import markdown
|
|
17
|
+
|
|
18
|
+
from wordpress_markdown_blog_loader.api import Post, Medium
|
|
19
|
+
from wordpress_markdown_blog_loader.api import Wordpress, WordpressEndpoint
|
|
20
|
+
from wordpress_markdown_blog_loader.remove_newlines import (
|
|
21
|
+
remove_newlines_from_paragraphs,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Blog(object):
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.dir: Path = None
|
|
28
|
+
self.path: Path = None
|
|
29
|
+
self.blog: frontmatter.Post = frontmatter.Post(content="")
|
|
30
|
+
self.uploaded_images: dict[str, Media] = {}
|
|
31
|
+
self.markdown_image_pattern = re.compile(
|
|
32
|
+
r'\!\[(?P<alt_text>[^]]*)\]\((?P<url>.*?)(?P<caption>\s*"[^"]*?")?\)'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def load(path: str) -> "Blog":
|
|
37
|
+
result = Blog()
|
|
38
|
+
result.path = Path(path)
|
|
39
|
+
result.dir = Path(path).parent
|
|
40
|
+
if not result.path.exists():
|
|
41
|
+
raise ValueError(f"{path} does not exist")
|
|
42
|
+
|
|
43
|
+
with open(path, "r") as f:
|
|
44
|
+
result.blog = frontmatter.load(f)
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
def save(self):
|
|
48
|
+
os.makedirs(self.dir, exist_ok=True)
|
|
49
|
+
with open(self.path, "wb") as f:
|
|
50
|
+
frontmatter.dump(self.blog, f)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def slug(self):
|
|
54
|
+
return self.blog.metadata.get("slug")
|
|
55
|
+
|
|
56
|
+
@slug.setter
|
|
57
|
+
def slug(self, slug):
|
|
58
|
+
self.blog.metadata["slug"] = slug
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def permalink_template(self):
|
|
62
|
+
return self.blog.metadata.get("permalink_template")
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def canonical(self):
|
|
66
|
+
return self.blog.metadata.get("canonical")
|
|
67
|
+
|
|
68
|
+
@canonical.setter
|
|
69
|
+
def canonical(self, value):
|
|
70
|
+
if value:
|
|
71
|
+
self.blog.metadata["canonical"] = value
|
|
72
|
+
else:
|
|
73
|
+
self.blog.metadata.pop("canonical", "")
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def focus_keywords(self):
|
|
77
|
+
"""
|
|
78
|
+
Keywords to determine SEO effectiveness. Can only be uploaded, not
|
|
79
|
+
retrieved. You just gotto love WP 😬
|
|
80
|
+
"""
|
|
81
|
+
return self.blog.metadata.get("focus-keywords")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@focus_keywords.setter
|
|
85
|
+
def focus_keywords(self, value):
|
|
86
|
+
if value:
|
|
87
|
+
self.blog.metadata["focus-keywords"] = " ".join(value) if isinstance(value, list) else value
|
|
88
|
+
else:
|
|
89
|
+
self.blog.metadata.pop("focus-keywords", "")
|
|
90
|
+
|
|
91
|
+
@permalink_template.setter
|
|
92
|
+
def permalink_template(self, permalink_template):
|
|
93
|
+
self.blog.metadata["permalink_template"] = permalink_template
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def author(self):
|
|
97
|
+
return self.blog.metadata.get("author")
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def author_id(self):
|
|
101
|
+
""" useful of the author has multiple user accounts on WP and we are not allowed to read
|
|
102
|
+
user's email addresses.
|
|
103
|
+
"""
|
|
104
|
+
return self.blog.metadata.get("author-id")
|
|
105
|
+
|
|
106
|
+
@author.setter
|
|
107
|
+
def author(self, author):
|
|
108
|
+
self.blog.metadata["author"] = author
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def title(self):
|
|
112
|
+
return self.blog.metadata.get("title")
|
|
113
|
+
|
|
114
|
+
@title.setter
|
|
115
|
+
def title(self, title):
|
|
116
|
+
self.blog.metadata["title"] = title
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def subtitle(self):
|
|
120
|
+
return self.blog.metadata.get("subtitle", "")
|
|
121
|
+
|
|
122
|
+
@subtitle.setter
|
|
123
|
+
def subtitle(self, subtitle):
|
|
124
|
+
self.blog.metadata["subtitle"] = subtitle
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def status(self):
|
|
128
|
+
return self.blog.metadata.get("status", "draft")
|
|
129
|
+
|
|
130
|
+
@status.setter
|
|
131
|
+
def status(self, status):
|
|
132
|
+
self.blog.metadata["status"] = status
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def content(self):
|
|
136
|
+
return self.blog.content
|
|
137
|
+
|
|
138
|
+
@content.setter
|
|
139
|
+
def content(self, content):
|
|
140
|
+
self.blog.content = content
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def og(self):
|
|
144
|
+
return self.blog.metadata.get("og", {})
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def image(self):
|
|
148
|
+
return self.blog.metadata.get("image")
|
|
149
|
+
|
|
150
|
+
@image.setter
|
|
151
|
+
def image(self, image):
|
|
152
|
+
self.blog.metadata["image"] = image
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def og_image(self):
|
|
156
|
+
return self.og.get("image")
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def brand(self):
|
|
160
|
+
return self.blog.metadata.get("brand")
|
|
161
|
+
|
|
162
|
+
@brand.setter
|
|
163
|
+
def brand(self, brand):
|
|
164
|
+
self.blog.metadata["brand"] = brand
|
|
165
|
+
|
|
166
|
+
@og_image.setter
|
|
167
|
+
def og_image(self, og_image):
|
|
168
|
+
if "og" in self.blog.metadata:
|
|
169
|
+
self.blog.metadata["og"]["image"] = og_image
|
|
170
|
+
else:
|
|
171
|
+
self.blog.metadata["og"] = {"image": og_image}
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def og_description(self):
|
|
175
|
+
return self.og.get("description", "")
|
|
176
|
+
|
|
177
|
+
@og_description.setter
|
|
178
|
+
def og_description(self, og_description):
|
|
179
|
+
if "og" in self.blog.metadata:
|
|
180
|
+
self.blog.metadata["og"]["description"] = og_description
|
|
181
|
+
else:
|
|
182
|
+
self.blog.metadata["og"] = {"description": og_description}
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def excerpt(self):
|
|
186
|
+
return self.blog.metadata.get("excerpt")
|
|
187
|
+
|
|
188
|
+
@excerpt.setter
|
|
189
|
+
def excerpt(self, excerpt):
|
|
190
|
+
self.blog.metadata["excerpt"] = excerpt
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def categories(self):
|
|
194
|
+
return self.blog.metadata.get("categories", [])
|
|
195
|
+
|
|
196
|
+
@categories.setter
|
|
197
|
+
def categories(self, categories: list[str]):
|
|
198
|
+
self.blog.metadata["categories"] = categories
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def industries(self):
|
|
202
|
+
return self.blog.metadata.get("industries", [])
|
|
203
|
+
|
|
204
|
+
@industries.setter
|
|
205
|
+
def industries(self, slugs: list[str]):
|
|
206
|
+
self.blog.metadata["industries"] = slugs
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def partners(self):
|
|
210
|
+
return self.blog.metadata.get("partners", [])
|
|
211
|
+
|
|
212
|
+
@partners.setter
|
|
213
|
+
def partners(self, slugs: list[str]):
|
|
214
|
+
self.blog.metadata["partners"] = slugs
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def capabilities(self) -> list[str]:
|
|
218
|
+
return self.blog.metadata.get("capabilities", [])
|
|
219
|
+
|
|
220
|
+
@capabilities.setter
|
|
221
|
+
def capabilities(self, slugs: list[str]):
|
|
222
|
+
self.blog.metadata["capabilities"] = slugs
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def tags(self):
|
|
226
|
+
return self.blog.metadata.get("tags", [])
|
|
227
|
+
|
|
228
|
+
@tags.setter
|
|
229
|
+
def tags(self, tags: list[str]):
|
|
230
|
+
self.blog.metadata["tags"] = tags
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def guid(self):
|
|
234
|
+
return self.blog.metadata.get("guid")
|
|
235
|
+
|
|
236
|
+
@guid.setter
|
|
237
|
+
def guid(self, new_guid):
|
|
238
|
+
self.blog.metadata["guid"] = new_guid
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def date(self) -> datetime:
|
|
242
|
+
return self.blog.metadata.get("date")
|
|
243
|
+
|
|
244
|
+
@date.setter
|
|
245
|
+
def date(self, new_date):
|
|
246
|
+
self.blog.metadata["date"] = new_date
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def image_path(self) -> Optional[Path]:
|
|
250
|
+
return Path(self.dir).joinpath(self.image) if self.image else None
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def email(self) -> Optional[str]:
|
|
254
|
+
return self.blog.metadata.get("email")
|
|
255
|
+
|
|
256
|
+
@email.setter
|
|
257
|
+
def email(self, new_email):
|
|
258
|
+
self.blog.metadata["email"] = new_email
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def og_image_path(self) -> Optional[Path]:
|
|
262
|
+
return Path(self.dir).joinpath(self.og_image) if self.og_image else None
|
|
263
|
+
|
|
264
|
+
def generate_og_image(self):
|
|
265
|
+
in_file = str(self.image_path)
|
|
266
|
+
|
|
267
|
+
if not self.og_image:
|
|
268
|
+
self.og_image = "images/og-banner.jpg"
|
|
269
|
+
out_file = str(self.og_image_path)
|
|
270
|
+
logging.info("generating new image in %s", out_file)
|
|
271
|
+
blog = ImageGeneratorBlog(self.title, self.subtitle, self.author, self.email)
|
|
272
|
+
generate_og_image(
|
|
273
|
+
blog,
|
|
274
|
+
in_file,
|
|
275
|
+
out_file,
|
|
276
|
+
overwrite=True,
|
|
277
|
+
gradient_magnitude=0.9,
|
|
278
|
+
brand=self.brand,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def banner(self) -> Optional["Image"]:
|
|
283
|
+
return Image.load(self.image_path) if self.image_path else None
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def og_banner(self) -> Optional["Image"]:
|
|
287
|
+
return Image.load(self.og_image_path) if self.og_image_path else None
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def rendered(self):
|
|
291
|
+
def replace_references(match: re.Match):
|
|
292
|
+
image = self.uploaded_images.get(match.group("url"))
|
|
293
|
+
if image:
|
|
294
|
+
caption = ""
|
|
295
|
+
if match.group("caption"):
|
|
296
|
+
caption = match.group("caption")
|
|
297
|
+
return f""
|
|
298
|
+
return match.group(0)
|
|
299
|
+
|
|
300
|
+
content = self.markdown_image_pattern.sub(replace_references, self.content)
|
|
301
|
+
html = markdown(
|
|
302
|
+
content, extensions=["fenced_code", "attr_list", "tables", "footnotes"]
|
|
303
|
+
)
|
|
304
|
+
return remove_newlines_from_paragraphs(html)
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def local_image_references(self) -> set[str]:
|
|
308
|
+
return set(
|
|
309
|
+
map(
|
|
310
|
+
lambda u: u.path,
|
|
311
|
+
filter(
|
|
312
|
+
lambda u: u.scheme in ["", "file"],
|
|
313
|
+
map(
|
|
314
|
+
lambda u: urlparse(u),
|
|
315
|
+
map(
|
|
316
|
+
lambda m: m.group("url"),
|
|
317
|
+
re.finditer(self.markdown_image_pattern, self.content),
|
|
318
|
+
),
|
|
319
|
+
),
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def remote_image_references(self, endpoint: WordpressEndpoint) -> set[ParseResult]:
|
|
325
|
+
return set(
|
|
326
|
+
filter(
|
|
327
|
+
lambda u: u.scheme in ["https", "http"]
|
|
328
|
+
and endpoint.is_host_for(u)
|
|
329
|
+
and u.path.startswith("/wp-content/uploads/"),
|
|
330
|
+
map(
|
|
331
|
+
lambda u: urlparse(u),
|
|
332
|
+
map(
|
|
333
|
+
lambda m: m.group("url"),
|
|
334
|
+
re.finditer(self.markdown_image_pattern, self.content),
|
|
335
|
+
),
|
|
336
|
+
),
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def download_media(
|
|
341
|
+
self,
|
|
342
|
+
url: Union[str, ParseResult],
|
|
343
|
+
wordpress: Wordpress,
|
|
344
|
+
path: Path,
|
|
345
|
+
):
|
|
346
|
+
url = urlparse(url) if isinstance(url, str) else url
|
|
347
|
+
logging.info("downloading %s as %s", url.geturl(), path.name)
|
|
348
|
+
raw = wordpress.get_media(url.geturl())
|
|
349
|
+
os.makedirs(path.parent, exist_ok=True)
|
|
350
|
+
with open(path, "wb") as file:
|
|
351
|
+
file.write(raw)
|
|
352
|
+
|
|
353
|
+
def download_remote_images(self, wp: Wordpress, slug: str = ""):
|
|
354
|
+
self.downloaded_images: dict[str, Path] = {}
|
|
355
|
+
for url in self.remote_image_references(wp.endpoint):
|
|
356
|
+
name = Path(url.path).name.removeprefix(slug)
|
|
357
|
+
name = name.removeprefix(slug)
|
|
358
|
+
path = Path("images").joinpath(name)
|
|
359
|
+
self.download_media(url, wp, Path(self.dir).joinpath(path))
|
|
360
|
+
self.downloaded_images[url.geturl()] = path
|
|
361
|
+
|
|
362
|
+
def replace_remote_image_references(match: re.Match):
|
|
363
|
+
url = match.group("url")
|
|
364
|
+
path = self.downloaded_images.get(url)
|
|
365
|
+
if path:
|
|
366
|
+
caption = ""
|
|
367
|
+
if match.group("caption"):
|
|
368
|
+
caption = match.group("caption")
|
|
369
|
+
return f""
|
|
370
|
+
return match.group(0)
|
|
371
|
+
|
|
372
|
+
self.content = self.markdown_image_pattern.sub(
|
|
373
|
+
replace_remote_image_references, self.content
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def upload_local_images(self, wp: Wordpress):
|
|
377
|
+
self.uploaded_images = {}
|
|
378
|
+
for filename in self.local_image_references:
|
|
379
|
+
path = Path(self.dir).joinpath(filename)
|
|
380
|
+
if not path.exists():
|
|
381
|
+
logging.warning("%s does not exist", path)
|
|
382
|
+
continue
|
|
383
|
+
|
|
384
|
+
slug = self.slug + "-" + re.sub(r"[/\.\\]+", "-", path.stem.strip("-"))
|
|
385
|
+
self.uploaded_images[filename] = wp.upload_media(slug, path)
|
|
386
|
+
|
|
387
|
+
def to_wordpress(self, wp: Wordpress) -> dict:
|
|
388
|
+
author = wp.get_unique_user_by_name(self.author, self.email, self.author_id)
|
|
389
|
+
self.upload_local_images(wp)
|
|
390
|
+
result = {
|
|
391
|
+
"title": self.title,
|
|
392
|
+
"slug": self.slug,
|
|
393
|
+
"author": author.id,
|
|
394
|
+
"date": self.date.isoformat(),
|
|
395
|
+
"date_gmt": self.date.astimezone(pytz.UTC).isoformat(),
|
|
396
|
+
"content": self.rendered,
|
|
397
|
+
"format": "standard",
|
|
398
|
+
"status": self.status,
|
|
399
|
+
"author": author.id,
|
|
400
|
+
"categories": [wp.get_category_id_by_name(c) for c in self.categories],
|
|
401
|
+
"tags": [wp.get_tag_id_by_name(c) for c in self.tags],
|
|
402
|
+
"acf": {"show_header_image": bool(self.image)},
|
|
403
|
+
"industries_taxonomy": [wp.get_industry_by_name(c) for c in self.industries],
|
|
404
|
+
"partners_taxonomy": [wp.get_partner_by_name(c) for c in self.partners],
|
|
405
|
+
"capabilities": [wp.get_capabilities_by_name(c) for c in self.capabilities],
|
|
406
|
+
}
|
|
407
|
+
if self.permalink_template:
|
|
408
|
+
result["permalink_template"] = self.permalink_template
|
|
409
|
+
|
|
410
|
+
if self.excerpt:
|
|
411
|
+
result["excerpt"] = markdown(
|
|
412
|
+
self.excerpt, extensions=["fenced_code", "attr_list"]
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
metadesc = self.og_description if self.og_description else self.excerpt
|
|
416
|
+
result["meta"] = {
|
|
417
|
+
"rank_math_facebook_description": metadesc,
|
|
418
|
+
"rank_math_twitter_description": metadesc,
|
|
419
|
+
}
|
|
420
|
+
if self.canonical:
|
|
421
|
+
result["meta"]["rank_math_canonical_url"] = self.canonical
|
|
422
|
+
|
|
423
|
+
if self.focus_keywords:
|
|
424
|
+
result["meta"]["rank_math_focus_keyword"] = ','.join(self.focus_keywords.split())
|
|
425
|
+
|
|
426
|
+
return result
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def from_wordpress(
|
|
430
|
+
post: Post, base_directory: Path, wordpress: Wordpress
|
|
431
|
+
) -> "Blog":
|
|
432
|
+
"""
|
|
433
|
+
convert a Wordpress post to a FrontMatter post
|
|
434
|
+
"""
|
|
435
|
+
blog = Blog()
|
|
436
|
+
blog.dir = (
|
|
437
|
+
Path(base_directory)
|
|
438
|
+
.joinpath(str(post.date.year))
|
|
439
|
+
.joinpath(f"{post.date.month:02d}")
|
|
440
|
+
.joinpath(post.slug)
|
|
441
|
+
)
|
|
442
|
+
blog.path = Path(blog.dir).joinpath("index.md")
|
|
443
|
+
|
|
444
|
+
if blog.path.exists():
|
|
445
|
+
blog = blog.load(blog.path)
|
|
446
|
+
|
|
447
|
+
blog.title = post.title
|
|
448
|
+
blog.author = wordpress.get_user_by_id(post.author).name
|
|
449
|
+
blog.guid = post.guid
|
|
450
|
+
blog.categories = [wordpress.categories_by_id[c] for c in post.categories]
|
|
451
|
+
blog.industries = [wordpress.industries_taxonomy_by_id[c] for c in post.industries_taxonomy]
|
|
452
|
+
blog.partners = [wordpress.partners_taxonomy_by_id[c] for c in post.partners_taxonomy]
|
|
453
|
+
blog.capabilities = [wordpress.capabilities_by_id[c] for c in post.capabilities]
|
|
454
|
+
if post.tags:
|
|
455
|
+
blog.tags = [wordpress.tags_by_id[t] for t in post.tags]
|
|
456
|
+
blog.date = post.date
|
|
457
|
+
blog.slug = post.slug
|
|
458
|
+
blog.status = post.status
|
|
459
|
+
blog.brand = wordpress.endpoint.host
|
|
460
|
+
blog.canonical = post.get("meta", {}).get(
|
|
461
|
+
"rank_math_canonical_url", blog.canonical
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
if keywords := post.get("meta", {}).get("rank_math_focus_keyword"):
|
|
465
|
+
blog.focus_keywords = keywords.split(",")
|
|
466
|
+
|
|
467
|
+
if post.permalink_template and not blog.permalink_template:
|
|
468
|
+
# keeping the permalink template registered in the blog metadata.
|
|
469
|
+
# Wordpress does not return the template that was set.
|
|
470
|
+
blog.permalink_template = post.permalink_template
|
|
471
|
+
|
|
472
|
+
if post.og_description:
|
|
473
|
+
blog.og_description = post.og_description
|
|
474
|
+
|
|
475
|
+
if post.featured_media:
|
|
476
|
+
if "acf" not in post:
|
|
477
|
+
logging.warning(
|
|
478
|
+
"Advanced Custom Field groups is not enabled for the REST API."
|
|
479
|
+
)
|
|
480
|
+
featured_media: Medium = Medium(wordpress.get("media", post.featured_media))
|
|
481
|
+
url = urlparse(featured_media.url)
|
|
482
|
+
|
|
483
|
+
if not blog.image:
|
|
484
|
+
blog.image = os.path.join("images", "banner" + Path(url.path).suffix)
|
|
485
|
+
blog.download_media(url.geturl(), wordpress, path=blog.image_path)
|
|
486
|
+
else:
|
|
487
|
+
featured_media: Medium = None
|
|
488
|
+
|
|
489
|
+
og_image = next(
|
|
490
|
+
filter(lambda u: wordpress.is_host_for((u)), post.og_images), None
|
|
491
|
+
)
|
|
492
|
+
if og_image and not (
|
|
493
|
+
featured_media and og_image.geturl() == featured_media.url
|
|
494
|
+
):
|
|
495
|
+
if not blog.og_image:
|
|
496
|
+
blog.og_image = os.path.join(
|
|
497
|
+
"images", "og-banner" + Path(og_image.path).suffix
|
|
498
|
+
)
|
|
499
|
+
blog.download_media(og_image.geturl(), wordpress, blog.og_image_path)
|
|
500
|
+
|
|
501
|
+
if post.excerpt:
|
|
502
|
+
blog.excerpt = markdownify.markdownify(
|
|
503
|
+
post.excerpt,
|
|
504
|
+
STRIP=True,
|
|
505
|
+
MARKDOWN_EXTENSIONS=[
|
|
506
|
+
"markdown.extensions.fenced_code",
|
|
507
|
+
"markdown.extensions.extra",
|
|
508
|
+
],
|
|
509
|
+
code_language_callback=_code_block_language,
|
|
510
|
+
).strip()
|
|
511
|
+
blog.content = markdownify.markdownify(
|
|
512
|
+
post.content,
|
|
513
|
+
STRIP=True,
|
|
514
|
+
MARKDOWN_EXTENSIONS=[
|
|
515
|
+
"markdown.extensions.fenced_code",
|
|
516
|
+
"markdown.extensions.extra",
|
|
517
|
+
"markdown.extensions.tables",
|
|
518
|
+
"markdown.extensions.footnotes",
|
|
519
|
+
],
|
|
520
|
+
code_language_callback=_code_block_language,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
return blog
|
|
524
|
+
|
|
525
|
+
def remove_empty_lines(self):
|
|
526
|
+
result = []
|
|
527
|
+
lines = self.content.splitlines(keepends=True)
|
|
528
|
+
in_code_block = False
|
|
529
|
+
for i, line in enumerate(lines):
|
|
530
|
+
if line.startswith("```"):
|
|
531
|
+
in_code_block = not in_code_block
|
|
532
|
+
|
|
533
|
+
if line != "\n":
|
|
534
|
+
result.append(line)
|
|
535
|
+
continue
|
|
536
|
+
|
|
537
|
+
if in_code_block:
|
|
538
|
+
if i + 1 >= len(lines) or not lines[i + 1].startswith("```"):
|
|
539
|
+
result.append(line)
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
if i + 1 < len(lines) and (
|
|
543
|
+
lines[i + 1].startswith("#")
|
|
544
|
+
or lines[i + 1].startswith("-")
|
|
545
|
+
or lines[i + 1].startswith("*")
|
|
546
|
+
or lines[i + 1].startswith("1.")
|
|
547
|
+
or lines[i + 1].startswith("~")
|
|
548
|
+
or lines[i + 1].startswith("`")
|
|
549
|
+
):
|
|
550
|
+
result.append(line)
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
if i + 2 < len(lines) and (
|
|
554
|
+
lines[i + 2].startswith("--") or lines[i + 2].startswith("==")
|
|
555
|
+
):
|
|
556
|
+
result.append(line)
|
|
557
|
+
continue
|
|
558
|
+
|
|
559
|
+
if i > 0 and (
|
|
560
|
+
lines[i - 1].startswith("--") or lines[i - 1].startswith("==")
|
|
561
|
+
):
|
|
562
|
+
result.append(line)
|
|
563
|
+
self.content = "".join(result)
|
|
564
|
+
|
|
565
|
+
def remove_span_tags(self):
|
|
566
|
+
self.content = remove_span_tags_from_code(self.content)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def remove_span_tags_from_code(markdown: str) -> str:
|
|
570
|
+
r"""
|
|
571
|
+
Removes <span> tags from the markdown code blocks. In some upgrade of Wordpress, the code
|
|
572
|
+
block was corrupted to contain the rendered HTML of the code block. This undoes that.
|
|
573
|
+
If the code block is for HTML, span is left in place.
|
|
574
|
+
|
|
575
|
+
>>> doc = "```python\n<span class=bla>spanned code</span>\n```\n"
|
|
576
|
+
>>> remove_span_tags_from_code(doc)
|
|
577
|
+
'```python\nspanned code\n```\n'
|
|
578
|
+
>>> doc = "```html\n<span class=bla>spanned code</span>\n```\n"
|
|
579
|
+
>>> remove_span_tags_from_code(doc)
|
|
580
|
+
'```html\n<span class=bla>spanned code</span>\n```\n'
|
|
581
|
+
"""
|
|
582
|
+
if "</span>" not in markdown:
|
|
583
|
+
return markdown
|
|
584
|
+
|
|
585
|
+
pattern = re.compile(
|
|
586
|
+
"(^```.*?$)(?P<code>.*?)(^```$)", flags=re.MULTILINE | re.DOTALL | re.VERBOSE
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
def _remove_span_tags(match):
|
|
590
|
+
if match.group(0).startswith("```html"):
|
|
591
|
+
return match.group(0)
|
|
592
|
+
|
|
593
|
+
return (
|
|
594
|
+
match.group(1)
|
|
595
|
+
+ re.sub(r"</?span[^>]*?>", "", match.group("code"))
|
|
596
|
+
+ match.group(3)
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
return pattern.sub(_remove_span_tags, markdown)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _code_block_language(code_block: bs4.element.Tag) -> str:
|
|
603
|
+
"""
|
|
604
|
+
determines the code block language from tag attribute class
|
|
605
|
+
>>> import bs4
|
|
606
|
+
>>> _code_block_language(bs4.BeautifulSoup('<code class="code language-python">...</code>', 'lxml'))
|
|
607
|
+
'python'
|
|
608
|
+
>>> _code_block_language(bs4.BeautifulSoup('<code class="code">...</code>', 'lxml'))
|
|
609
|
+
''
|
|
610
|
+
"""
|
|
611
|
+
code_tag = code_block.find("code")
|
|
612
|
+
return next(
|
|
613
|
+
map(
|
|
614
|
+
lambda l: l.replace("language-", ""),
|
|
615
|
+
filter(
|
|
616
|
+
lambda c: c.startswith("language-"),
|
|
617
|
+
code_tag.get("class", []) if code_tag else "",
|
|
618
|
+
),
|
|
619
|
+
),
|
|
620
|
+
"",
|
|
621
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from bs4 import BeautifulSoup
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def check_links(content: str):
|
|
6
|
+
"""
|
|
7
|
+
Checks a given URL for broken links in its HTML document.
|
|
8
|
+
Returns a list of all broken links found.
|
|
9
|
+
"""
|
|
10
|
+
soup = BeautifulSoup(content, "html.parser")
|
|
11
|
+
|
|
12
|
+
broken_links = []
|
|
13
|
+
for link in soup.find_all("a"):
|
|
14
|
+
href = link.get("href")
|
|
15
|
+
if href and href.startswith("http"):
|
|
16
|
+
try:
|
|
17
|
+
response = requests.get(href, headers={"User-Agent": "curl/7.86.0"})
|
|
18
|
+
if response.status_code in [400, 404]:
|
|
19
|
+
broken_links.append((response.status_code, href))
|
|
20
|
+
except requests.exceptions.RequestException:
|
|
21
|
+
broken_links.append((-400, href))
|
|
22
|
+
|
|
23
|
+
return broken_links
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
import click
|
|
29
|
+
|
|
30
|
+
from wordpress_markdown_blog_loader.api import Wordpress, Post
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.command(name="check-links")
|
|
34
|
+
@click.option(
|
|
35
|
+
"--host",
|
|
36
|
+
type=str,
|
|
37
|
+
required=True,
|
|
38
|
+
nargs=1,
|
|
39
|
+
help="wordpress host to check blog post links of",
|
|
40
|
+
)
|
|
41
|
+
@click.argument(
|
|
42
|
+
"post-id",
|
|
43
|
+
type=int,
|
|
44
|
+
nargs=-1,
|
|
45
|
+
)
|
|
46
|
+
def command(host: str, post_id: tuple[str]):
|
|
47
|
+
"""
|
|
48
|
+
check for broken links in WordPress posts
|
|
49
|
+
"""
|
|
50
|
+
wordpress = Wordpress(host)
|
|
51
|
+
wordpress.connect()
|
|
52
|
+
|
|
53
|
+
if post_id:
|
|
54
|
+
posts = list(
|
|
55
|
+
map(
|
|
56
|
+
lambda p: Post(p) if p else None,
|
|
57
|
+
map(
|
|
58
|
+
lambda p: wordpress.get("posts", p, ({"context": "edit"})), post_id
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
for i, post in enumerate(posts):
|
|
64
|
+
if not post:
|
|
65
|
+
logging.error("post with id %d was not found", post_id[i])
|
|
66
|
+
exit(1)
|
|
67
|
+
else:
|
|
68
|
+
posts = wordpress.posts({"context": "edit"})
|
|
69
|
+
|
|
70
|
+
for post in posts:
|
|
71
|
+
broken_links = check_links(post.content)
|
|
72
|
+
if broken_links:
|
|
73
|
+
logging.error("post %s has %d broken links", post.link, len(broken_links))
|
|
74
|
+
for link in broken_links:
|
|
75
|
+
logging.error(" %s", link)
|
|
76
|
+
else:
|
|
77
|
+
logging.info("post %s has no broken links", post.link)
|