openwrite 0.17.1__tar.gz → 0.17.2__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 (100) hide show
  1. {openwrite-0.17.1/openwrite.egg-info → openwrite-0.17.2}/PKG-INFO +1 -1
  2. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/gemini_single.py +26 -25
  3. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/routes/blog.py +10 -5
  4. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/routes/dashboard.py +9 -4
  5. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/routes/upload.py +2 -7
  6. openwrite-0.17.2/openwrite/static/feed/default.xml +2 -0
  7. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/feed/testx.xml +1 -1
  8. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/script/post_preview.js +66 -6
  9. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/new_post.html +1 -1
  10. {openwrite-0.17.1 → openwrite-0.17.2/openwrite.egg-info}/PKG-INFO +1 -1
  11. {openwrite-0.17.1 → openwrite-0.17.2}/pyproject.toml +1 -1
  12. openwrite-0.17.1/openwrite/static/feed/default.xml +0 -2
  13. {openwrite-0.17.1 → openwrite-0.17.2}/LICENSE +0 -0
  14. {openwrite-0.17.1 → openwrite-0.17.2}/MANIFEST.in +0 -0
  15. {openwrite-0.17.1 → openwrite-0.17.2}/README.md +0 -0
  16. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/__init__.py +0 -0
  17. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/cli.py +0 -0
  18. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/db/__init__.py +0 -0
  19. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/db/base.py +0 -0
  20. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/gemini_multi.py +0 -0
  21. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/routes/admin.py +0 -0
  22. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/routes/auth.py +0 -0
  23. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/routes/federation.py +0 -0
  24. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/routes/main.py +0 -0
  25. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  26. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/Merriweather.ttf +0 -0
  27. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/avatar.png +0 -0
  28. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/emoji.json +0 -0
  29. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/favicon.ico +0 -0
  30. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/favicon.png +0 -0
  31. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/feed/.gitkeep +0 -0
  32. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/feed/test.xml +0 -0
  33. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/logo.png +0 -0
  34. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/script/chart.js +0 -0
  35. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/script/highlight.min.js +0 -0
  36. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/script/nativeEmoji.js +0 -0
  37. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/script/openwrite.js +0 -0
  38. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/script/papaparse.min.js +0 -0
  39. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/script/showdown.min.js +0 -0
  40. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/script/showdown.min.js.map +0 -0
  41. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/script/site.compat.min.js +0 -0
  42. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/script/site.min.js +0 -0
  43. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/blog_design.css +0 -0
  44. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/default.css +0 -0
  45. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/hl.css +0 -0
  46. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/nativeEmoji.min.css +0 -0
  47. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/style.css +0 -0
  48. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/themes/cyberpunk.css +0 -0
  49. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/themes/light.css +0 -0
  50. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/themes/obscura.css +0 -0
  51. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/themes/paper.css +0 -0
  52. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/themes/sepia.css +0 -0
  53. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/themes/stickynote.css +0 -0
  54. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/themes/synthwave.css +0 -0
  55. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/themes/terminal.css +0 -0
  56. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/themes/warmnight.css +0 -0
  57. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/themes/windows98.css +0 -0
  58. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/static/style/toc.css +0 -0
  59. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/admin.html +0 -0
  60. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/blog.html +0 -0
  61. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/blog_footer.html +0 -0
  62. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/blog_header.html +0 -0
  63. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/blog_preview.html +0 -0
  64. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/changepw.html +0 -0
  65. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/create.html +0 -0
  66. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/dashboard.html +0 -0
  67. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/discover.html +0 -0
  68. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/edit.html +0 -0
  69. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/footer.html +0 -0
  70. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/header.html +0 -0
  71. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/import.html +0 -0
  72. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/index.html +0 -0
  73. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/instances.html +0 -0
  74. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/login.html +0 -0
  75. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/new_page.html +0 -0
  76. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/page_preview.html +0 -0
  77. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/post.html +0 -0
  78. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/preview.html +0 -0
  79. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/register.html +0 -0
  80. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/settings.html +0 -0
  81. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/stats.html +0 -0
  82. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/templates/translations.html +0 -0
  83. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/utils/activitypub.py +0 -0
  84. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/utils/create_db.py +0 -0
  85. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/utils/db.py +0 -0
  86. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/utils/helpers.py +0 -0
  87. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/utils/i18n.json +0 -0
  88. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/utils/migrations.py +0 -0
  89. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/utils/models.py +0 -0
  90. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/utils/tags.py +0 -0
  91. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/utils/toc.py +0 -0
  92. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/utils/valkey_client.py +0 -0
  93. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite/utils/worker.py +0 -0
  94. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite.egg-info/SOURCES.txt +0 -0
  95. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite.egg-info/dependency_links.txt +0 -0
  96. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite.egg-info/entry_points.txt +0 -0
  97. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite.egg-info/requires.txt +0 -0
  98. {openwrite-0.17.1 → openwrite-0.17.2}/openwrite.egg-info/top_level.txt +0 -0
  99. {openwrite-0.17.1 → openwrite-0.17.2}/setup.cfg +0 -0
  100. {openwrite-0.17.1 → openwrite-0.17.2}/tests/test_federation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.17.1
3
+ Version: 0.17.2
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -1,7 +1,8 @@
1
1
  import os, subprocess
2
+ import re
2
3
  from dotenv import load_dotenv
3
4
  from openwrite.utils.db import init_engine, SessionLocal
4
- from openwrite.utils.models import Blog, Post, User, View
5
+ from openwrite.utils.models import Blog, Post, User, View, Page
5
6
  from openwrite.utils.helpers import anonymize
6
7
  from md2gemini import md2gemini
7
8
  from datetime import datetime, timezone
@@ -11,28 +12,28 @@ def blog_index(req):
11
12
  global session
12
13
  try:
13
14
  blog = session.query(Blog).filter_by(name="default").first()
14
- if not blog:
15
- return "not found"
16
- posts = (session.query(Post)
17
- .filter_by(blog=blog.id)
18
- .order_by(Post.id.desc())
19
- .all())
20
- pages = (session.query(Page)
21
- .filter(Page.blog == blog.id, Page.show == "1").all())
22
- homepage = session.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
23
- body = f"# 📓 {blog.title}\n\n"
24
- posts_data = ""
25
- for post in posts:
26
- posts_data += f"=> /b/{blogname}/{post.link} {post.title}\n\n"
27
- pages_data = ""
28
- for page in pages:
29
- pages_data += f"=> /b/{blogname}/{page.url} {page.name}\n\n"
30
- content = pages_data + "\n"
31
- content += homepage.content_raw.replace('{posts}', f"\n{posts_data}")
32
- content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
33
- body += md2gemini(content)
34
-
35
- return body
15
+ if not blog:
16
+ return "not found"
17
+ posts = (session.query(Post)
18
+ .filter_by(blog=blog.id, isdraft="0")
19
+ .order_by(Post.id.desc())
20
+ .all())
21
+ pages = (session.query(Page)
22
+ .filter(Page.blog == blog.id, Page.show == "1").all())
23
+ homepage = session.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
24
+ body = f"# 📓 {blog.title}\n\n"
25
+ posts_data = ""
26
+ for post in posts:
27
+ posts_data += f"=> /p/{post.link} {post.title}\n\n"
28
+ pages_data = ""
29
+ for page in pages:
30
+ pages_data += f"=> /p/{page.url} {page.name}\n\n"
31
+ content = pages_data + "\n"
32
+ content += homepage.content_raw.replace('{posts}', f"\n{posts_data}") if homepage else posts_data
33
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
34
+ body += md2gemini(content)
35
+
36
+ return body
36
37
  finally:
37
38
  session.close()
38
39
 
@@ -55,7 +56,7 @@ def blog_post(req):
55
56
  .filter_by(blog=blog.id, url=slug)
56
57
  .first())
57
58
  if page:
58
- content = f"=> /b/{blog.name} 📓 {blog.title}\n\n"
59
+ content = f"=> / 📓 {blog.title}\n\n"
59
60
  content += page.content_raw
60
61
  content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
61
62
  return md2gemini(content)
@@ -72,7 +73,7 @@ def blog_post(req):
72
73
  session.commit()
73
74
 
74
75
  post.authorname = user.username
75
- gemtext = f"=> /b/{blog.name} 📓 {blog.title}\n"
76
+ gemtext = f"=> / 📓 {blog.title}\n"
76
77
  gemtext += f"# 🧾 {post.title}\n\n"
77
78
  gemtext += f"{post.date} "
78
79
  if post.author != "0":
@@ -1,4 +1,4 @@
1
- from flask import Blueprint, render_template, redirect, request, g, Response, abort, send_file, jsonify
1
+ from flask import Blueprint, render_template, redirect, request, g, Response, abort, send_file, jsonify, current_app
2
2
  from openwrite.utils.models import Blog, Post, User, View, Like, Page, PostDraft, Comment
3
3
  from openwrite.utils.tags import get_post_tags, parse_tags_from_posts_syntax, get_posts_by_tags
4
4
  from openwrite.utils.helpers import gen_link, sanitize_html, anonymize, get_ip
@@ -457,10 +457,15 @@ def like():
457
457
 
458
458
 
459
459
  def serve_rss(blog):
460
- """
461
- Here we just serve static RSS files :)
462
- """
463
- return send_file(f"{g.mainpath}/static/feed/{blog.name}.xml")
460
+ """Generate a missing feed, then serve the cached file as usual."""
461
+ if blog is None:
462
+ abort(404)
463
+ directory = current_app.config.get('RSS_DIRECTORY', f'{g.mainpath}/static/feed')
464
+ path = os.path.join(directory, f'{blog.name}.xml')
465
+ if not os.path.isfile(path):
466
+ from openwrite.routes.dashboard import generate_rss
467
+ generate_rss(blog.name)
468
+ return send_file(path, mimetype='application/rss+xml')
464
469
 
465
470
 
466
471
  @blog_bp.route("/comment", methods=['POST'])
@@ -1,4 +1,4 @@
1
- from flask import Blueprint, render_template, redirect, request, g, abort, Response
1
+ from flask import Blueprint, render_template, redirect, request, g, abort, Response, current_app
2
2
  from openwrite.utils.models import Blog, Post, User, View, Like, Page, Settings, Tag, PostTag
3
3
  from openwrite.utils.helpers import sanitize_html, gen_link, safe_css, is_html, get_themes
4
4
  from openwrite.utils.tags import parse_tags_from_input, update_post_tags, get_post_tags
@@ -14,6 +14,7 @@ from zoneinfo import ZoneInfo
14
14
  from cryptography.hazmat.primitives.asymmetric import rsa
15
15
  from cryptography.hazmat.primitives import serialization
16
16
  import json
17
+ import os
17
18
  import bcrypt
18
19
  import markdown
19
20
  from bs4 import BeautifulSoup
@@ -672,7 +673,10 @@ def generate_rss(blog):
672
673
  fg.image(url="https://openwrite.io/static/favicon.png")
673
674
  fg.title(b.title)
674
675
 
675
- if b.access == "domain":
676
+ if g.mode == "single":
677
+ blog_url = f"https://{g.main_domain}"
678
+ post_base_url = f"{blog_url}/p"
679
+ elif b.access == "domain":
676
680
  blog_url = f"https://{b.name}.{g.main_domain}"
677
681
  post_base_url = blog_url
678
682
  else:
@@ -694,5 +698,6 @@ def generate_rss(blog):
694
698
  fe.content(content=p.content_html, type="CDATA")
695
699
  fe.published(p.date.replace(tzinfo=timezone.utc))
696
700
 
697
- rssfeed = fg.rss_str(pretty=True)
698
- fg.rss_file(f"{g.mainpath}/static/feed/{b.name}.xml")
701
+ directory = current_app.config.get('RSS_DIRECTORY', f'{g.mainpath}/static/feed')
702
+ os.makedirs(directory, exist_ok=True)
703
+ fg.rss_file(os.path.join(directory, f'{b.name}.xml'))
@@ -1,9 +1,8 @@
1
1
  from flask import Blueprint, request, jsonify, g, send_from_directory, abort
2
- from openwrite.utils.models import User
3
2
  from werkzeug.utils import secure_filename
4
3
  from PIL import Image
5
4
  import os
6
- import hashlib
5
+ from uuid import uuid4
7
6
  import requests
8
7
 
9
8
  upload_bp = Blueprint("upload", __name__)
@@ -43,11 +42,7 @@ def upload_image():
43
42
  return jsonify({"error": g.trans['not_image']}), 400
44
43
 
45
44
  file.stream.seek(0)
46
- user = g.db.query(User).filter_by(id=g.user).first()
47
-
48
- m = hashlib.md5()
49
- m.update(filename.encode() + user.username.encode())
50
- filename = f"{m.hexdigest()}.{extension}"
45
+ filename = f"{uuid4().hex}.{extension}"
51
46
 
52
47
  if STORAGE_BACKEND == "bunny":
53
48
  url = f"https://storage.bunnycdn.com/{BUNNY_ZONE}/{filename}"
@@ -0,0 +1,2 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><title>singletest.open.write</title><link>https://singletest.open.write</link><description>x</description><docs>http://www.rssboard.org/rss-specification</docs><generator>python-feedgen</generator><image><url>https://openwrite.io/static/favicon.png</url><title>singletest.open.write</title><link>https://singletest.open.write</link></image><lastBuildDate>Sat, 07 Nov 2026 22:26:48 +0000</lastBuildDate><ttl>5</ttl><item><title>this is a test</title><link>https://singletest.open.write/p/this-is-a-test</link><description>test...</description><content:encoded><![CDATA[<p>test</p>]]></content:encoded><pubDate>Sat, 07 Nov 2026 22:26:48 +0000</pubDate></item><item><title>test</title><link>https://singletest.open.write/p/test-2</link><description>test2...</description><content:encoded><![CDATA[<p>test2</p>]]></content:encoded><pubDate>Sat, 07 Nov 2026 22:26:48 +0000</pubDate></item><item><title>test</title><link>https://singletest.open.write/p/test</link><description>test...</description><content:encoded><![CDATA[<p>test</p>]]></content:encoded><pubDate>Sat, 07 Nov 2026 22:26:48 +0000</pubDate></item></channel></rss>
@@ -1,2 +1,2 @@
1
1
  <?xml version='1.0' encoding='UTF-8'?>
2
- <rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><title>testx</title><link>https://multitest.open.write/b/testx</link><description>x</description><docs>http://www.rssboard.org/rss-specification</docs><generator>python-feedgen</generator><image><url>https://openwrite.io/static/favicon.png</url><title>testx</title><link>https://multitest.open.write/b/testx</link></image><lastBuildDate>Sat, 07 Nov 2026 21:25:55 +0000</lastBuildDate><ttl>5</ttl><item><title>this is a test</title><link>https://multitest.open.write/b/testx/this-is-a-test</link><description>test2...</description><content:encoded><![CDATA[<p>test2</p>]]></content:encoded><pubDate>Sat, 07 Nov 2026 21:25:55 +0000</pubDate></item></channel></rss>
2
+ <rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><title>testx</title><link>https://multitest.open.write/b/testx</link><description>x</description><docs>http://www.rssboard.org/rss-specification</docs><generator>python-feedgen</generator><image><url>https://openwrite.io/static/favicon.png</url><title>testx</title><link>https://multitest.open.write/b/testx</link></image><lastBuildDate>Sat, 07 Nov 2026 22:26:48 +0000</lastBuildDate><ttl>5</ttl><item><title>this is a test</title><link>https://multitest.open.write/b/testx/this-is-a-test</link><description>test2...</description><content:encoded><![CDATA[<p>test2</p>]]></content:encoded><pubDate>Sat, 07 Nov 2026 22:26:48 +0000</pubDate></item></channel></rss>
@@ -10,6 +10,12 @@
10
10
  placeholder.textContent = frame.dataset.placeholder;
11
11
  placeholder.className = 'placeholder';
12
12
  let timer;
13
+ let previewPort;
14
+ let latestHtml = '';
15
+
16
+ function sendPreview() {
17
+ if (previewPort) previewPort.postMessage(latestHtml);
18
+ }
13
19
 
14
20
  function withToc(html) {
15
21
  // The template stays detached: authored HTML is never inserted into the editor.
@@ -60,16 +66,31 @@
60
66
  if (!toggle.checked) return;
61
67
  let html = editor.value.trim() ? converter.makeHtml(editor.value) : placeholder.outerHTML;
62
68
  if (tocToggle.checked) html = withToc(html);
63
- // No script or same-origin permissions: authored HTML stays inside the frame.
64
- // CSP also prevents embedded frames, forms, and external styles from loading.
69
+ latestHtml = html;
70
+ if (frame.hasAttribute('srcdoc')) {
71
+ sendPreview();
72
+ return;
73
+ }
74
+ // Load the frame once. Only our nonce-protected updater may run; authored
75
+ // scripts stay blocked and the frame has no same-origin permissions.
76
+ const nonce = Array.from(crypto.getRandomValues(new Uint8Array(16)), byte => byte.toString(16).padStart(2, '0')).join('');
77
+ frame.addEventListener('load', () => {
78
+ // Connect only the initial, empty document. A port stays with that
79
+ // document, so later iframe navigation cannot receive draft content.
80
+ const channel = new MessageChannel();
81
+ previewPort = channel.port1;
82
+ frame.contentWindow.postMessage('openwrite-preview-connect', '*', [channel.port2]);
83
+ sendPreview();
84
+ }, {once: true});
65
85
  frame.srcdoc = `<!doctype html><html><head><meta charset="utf-8">
66
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src http: https: data:; media-src http: https:; base-uri 'none'; form-action 'none'">
86
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'unsafe-inline'; img-src http: https: data:; media-src http: https:; base-uri 'none'; form-action 'none'">
67
87
  <style>
68
88
  * { box-sizing: border-box; }
89
+ html { overflow-anchor: none; scroll-behavior: auto; }
69
90
  body { margin: 0; padding: 0 1rem 1rem; color: #e9d4ce;
70
91
  font: 16px/1.6 system-ui, sans-serif; overflow-wrap: anywhere; }
71
92
  h1, h2, h3, h4, h5, h6 { line-height: 1.3; margin: 1em 0 .5em; }
72
- body > :first-child { margin-top: 0; }
93
+ #preview-content > :first-child { margin-top: 0; }
73
94
  p, ul, ol, pre, blockquote, table { margin: 0 0 1em; }
74
95
  a { color: #b8a0eb; }
75
96
  img, video { max-width: 100%; height: auto; }
@@ -84,7 +105,47 @@
84
105
  .post-toc ul { margin: .35rem 0 0; padding-left: 1.25rem; }
85
106
  .post-toc li { margin: .15rem 0; }
86
107
  [id^="post-section-"] { scroll-margin-top: 1rem; }
87
- </style></head><body>${html}</body></html>`;
108
+ </style></head><body><div id="preview-content"></div>
109
+ <script nonce="${nonce}">
110
+ (() => {
111
+ const content = document.getElementById('preview-content');
112
+ window.addEventListener('message', event => {
113
+ if (event.source !== parent || event.data !== 'openwrite-preview-connect' || !event.ports[0]) return;
114
+ event.ports[0].onmessage = ({data}) => {
115
+ if (typeof data !== 'string') return;
116
+ const x = window.scrollX, y = window.scrollY;
117
+ const template = document.createElement('template');
118
+ template.innerHTML = data;
119
+ template.content.querySelectorAll('script, meta, base, link, iframe, object, embed').forEach(node => node.remove());
120
+ // Keep unchanged blocks (especially loaded images) in place.
121
+ const oldNodes = [...content.childNodes];
122
+ const newNodes = [...template.content.childNodes];
123
+ let start = 0, oldEnd = oldNodes.length, newEnd = newNodes.length;
124
+ while (start < oldEnd && start < newEnd && oldNodes[start].isEqualNode(newNodes[start])) start++;
125
+ while (oldEnd > start && newEnd > start && oldNodes[oldEnd - 1].isEqualNode(newNodes[newEnd - 1])) { oldEnd--; newEnd--; }
126
+ const fragment = document.createDocumentFragment();
127
+ for (let i = start; i < newEnd; i++) fragment.append(newNodes[i]);
128
+ const range = document.createRange();
129
+ range.setStart(content, start);
130
+ range.setEnd(content, oldEnd);
131
+ range.deleteContents();
132
+ range.insertNode(fragment);
133
+ window.scrollTo({left: x, top: y, behavior: 'instant'});
134
+ };
135
+ }, {once: true});
136
+ // Keep TOC links inside this document; other links must not replace it.
137
+ document.addEventListener('click', event => {
138
+ const link = event.target.closest('a');
139
+ if (!link) return;
140
+ event.preventDefault();
141
+ const href = link.getAttribute('href') || '';
142
+ if (href.startsWith('about:srcdoc#') || href.startsWith('#')) {
143
+ const id = href.slice(href.indexOf('#') + 1);
144
+ document.getElementById(id)?.scrollIntoView();
145
+ }
146
+ });
147
+ })();
148
+ </script></body></html>`;
88
149
  }
89
150
 
90
151
  toggle.addEventListener('change', () => {
@@ -92,7 +153,6 @@
92
153
  panel.hidden = !toggle.checked;
93
154
  layout.classList.toggle('has-preview', toggle.checked);
94
155
  if (toggle.checked) render();
95
- else frame.removeAttribute('srcdoc');
96
156
  });
97
157
 
98
158
  editor.addEventListener('input', () => {
@@ -46,7 +46,7 @@
46
46
  <div class="post-editor" id="post-editor">
47
47
  <textarea autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" name="content" class="post_content" placeholder="{{ _('content') }}">{% if form_data %}{{ form_data.get('content_raw', '') }}{% elif post %}{{ post.content_raw }}{% endif %}</textarea>
48
48
  <section class="live-preview-panel" id="live-preview-panel" aria-label="{{ live_preview_label }}" hidden>
49
- <iframe id="live-preview-frame" title="{{ live_preview_label }}" sandbox="" referrerpolicy="no-referrer" data-placeholder="{{ preview_hint }}"></iframe>
49
+ <iframe id="live-preview-frame" title="{{ live_preview_label }}" sandbox="allow-scripts" referrerpolicy="no-referrer" data-placeholder="{{ preview_hint }}"></iframe>
50
50
  </section>
51
51
  </div>
52
52
  <input type="hidden" name="content_raw" id="content_raw" value="{% if post %}{{ post.content_raw }}{% endif %}">
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.17.1
3
+ Version: 0.17.2
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openwrite"
7
- version = "0.17.1"
7
+ version = "0.17.2"
8
8
  license = { file = "LICENSE" }
9
9
  dependencies = [
10
10
  "flask",
@@ -1,2 +0,0 @@
1
- <?xml version='1.0' encoding='UTF-8'?>
2
- <rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><title>singletest.open.write</title><link>https://default.singletest.open.write</link><description>x</description><docs>http://www.rssboard.org/rss-specification</docs><generator>python-feedgen</generator><image><url>https://openwrite.io/static/favicon.png</url><title>singletest.open.write</title><link>https://default.singletest.open.write</link></image><lastBuildDate>Sat, 07 Nov 2026 21:25:56 +0000</lastBuildDate><ttl>5</ttl><item><title>this is a test</title><link>https://default.singletest.open.write/this-is-a-test</link><description>test...</description><content:encoded><![CDATA[<p>test</p>]]></content:encoded><pubDate>Sat, 07 Nov 2026 21:25:56 +0000</pubDate></item><item><title>test</title><link>https://default.singletest.open.write/test-2</link><description>test2...</description><content:encoded><![CDATA[<p>test2</p>]]></content:encoded><pubDate>Sat, 07 Nov 2026 21:25:56 +0000</pubDate></item><item><title>test</title><link>https://default.singletest.open.write/test</link><description>test...</description><content:encoded><![CDATA[<p>test</p>]]></content:encoded><pubDate>Sat, 07 Nov 2026 21:25:56 +0000</pubDate></item></channel></rss>
File without changes
File without changes
File without changes
File without changes
File without changes