nnw-theme 0.0.0 → 0.1.0
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.
- package/LICENSE +201 -0
- package/README.md +75 -1
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/assets/fixtures/article.toml +24 -0
- package/assets/fixtures/kitchen-sink.toml +36 -0
- package/assets/footnotes.js +174 -0
- package/assets/guide/design-checklist.md +19 -0
- package/assets/guide/fixtures.md +42 -0
- package/assets/guide/publishing.md +29 -0
- package/assets/guide/skill.md +44 -0
- package/assets/guide/theme-format.md +52 -0
- package/assets/netnewswire/LICENSE +21 -0
- package/assets/netnewswire/Mac/main_mac.js +43 -0
- package/assets/netnewswire/Mac/page.html +12 -0
- package/assets/netnewswire/Shared/core.css +186 -0
- package/assets/netnewswire/Shared/main.js +221 -0
- package/assets/netnewswire/Shared/newsfoot.js +173 -0
- package/assets/netnewswire/iOS/main_ios.js +520 -0
- package/assets/netnewswire/iOS/page.html +19 -0
- package/assets/netnewswire/netnewswire.json +46 -0
- package/assets/stubs/.agents/skills/creating-nnw-themes/SKILL.md +22 -0
- package/assets/stubs/.agents/skills/creating-nnw-themes/agents/openai.yaml +4 -0
- package/assets/stubs/.github/workflows/check.yml +14 -0
- package/assets/stubs/.github/workflows/pages.yml +24 -0
- package/assets/stubs/.github/workflows/release.yml +24 -0
- package/assets/stubs/.github/workflows/screenshot.yml +27 -0
- package/assets/stubs/AGENTS.md +24 -0
- package/dist/browser.js +227 -0
- package/dist/cli.js +3 -0
- package/dist/commands/bump.js +23 -0
- package/dist/commands/capture.js +26 -0
- package/dist/commands/check.js +67 -0
- package/dist/commands/completion.js +8 -0
- package/dist/commands/guide.js +9 -0
- package/dist/commands/init.js +155 -0
- package/dist/commands/marketplace.js +22 -0
- package/dist/commands/package.js +16 -0
- package/dist/commands/preview.js +49 -0
- package/dist/commands/progress.js +31 -0
- package/dist/commands/release-check.js +45 -0
- package/dist/commands/render.js +17 -0
- package/dist/commands/screenshot.js +36 -0
- package/dist/commands/setup.js +4 -0
- package/dist/commands/update.js +7 -0
- package/dist/commands.js +175 -0
- package/dist/completion.js +203 -0
- package/dist/interactive.js +32 -0
- package/dist/main.js +229 -0
- package/dist/netnewswire.js +84 -0
- package/dist/package.js +28 -0
- package/dist/plist.js +175 -0
- package/dist/project.js +195 -0
- package/dist/pyformat.js +82 -0
- package/dist/render.js +516 -0
- package/dist/stubs.js +49 -0
- package/dist/urlparse.js +32 -0
- package/dist/validate.js +259 -0
- package/dist/zip.js +72 -0
- package/lldb/nnwdump.py +151 -0
- package/package.json +55 -2
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Create NetNewsWire themes
|
|
2
|
+
|
|
3
|
+
This guide is for people and agents building a theme with `nnw-theme`. Run the tool
|
|
4
|
+
as `npx nnw-theme@1 <command>`; never install it globally or add it to the theme
|
|
5
|
+
repository. Read another topic with `npx nnw-theme@1 guide <topic>`: `theme-format`,
|
|
6
|
+
`fixtures`, `design-checklist`, or `publishing`.
|
|
7
|
+
|
|
8
|
+
The upstream `dave-atx/netnewswire-theme-template` repository is a GitHub template.
|
|
9
|
+
Work in the person's own repository made via **Use this template**, not in the
|
|
10
|
+
upstream template or a fork. If they already have their own copy open, use it.
|
|
11
|
+
Otherwise, help them create one from the template if you can, or ask them to create
|
|
12
|
+
and open it; confirm before creating a GitHub repository. Then work on its single
|
|
13
|
+
root-level `*.nnwtheme` bundle. If `.nnw-theme-uninitialized` exists, run the guided
|
|
14
|
+
`npx nnw-theme@1 init` before design work; let the person approve the permanent
|
|
15
|
+
identifier even when other identity details are already known.
|
|
16
|
+
|
|
17
|
+
Collect or infer a compact design brief: mood, typography, color direction, and any
|
|
18
|
+
specific reading needs. Prefer changing the CSS variables and rules in
|
|
19
|
+
`stylesheet.css`. Edit `template.html` only when the requested structure or behavior
|
|
20
|
+
needs it. For format constraints and macro behavior, read the `theme-format` topic.
|
|
21
|
+
To preview content the built-in articles lack, such as a real feed's markup or
|
|
22
|
+
footnotes, add a fixture as the `fixtures` topic describes.
|
|
23
|
+
|
|
24
|
+
Iterate with this loop:
|
|
25
|
+
|
|
26
|
+
1. Make a focused theme edit.
|
|
27
|
+
2. Run `npx nnw-theme@1 render` for fast generated pages or
|
|
28
|
+
`npx nnw-theme@1 screenshot` when a checked image is needed.
|
|
29
|
+
3. Inspect relevant light/dark and device previews under `build/preview/`.
|
|
30
|
+
4. Explain what changed in visual terms and incorporate the person’s reaction.
|
|
31
|
+
|
|
32
|
+
Use the `design-checklist` topic during final visual review. Always finish
|
|
33
|
+
implementation with `npx nnw-theme@1 check`; do not substitute a Chromium result for
|
|
34
|
+
the required WebKit gate. Give the person a concrete preview or screenshot path they
|
|
35
|
+
can open. If `check` reports that WebKit is missing, run `npx nnw-theme@1 setup`.
|
|
36
|
+
|
|
37
|
+
Treat the root bundle filename and `ThemeIdentifier` as permanent after the first
|
|
38
|
+
release. Increase the integer `Version` with `npx nnw-theme@1 bump` for each release.
|
|
39
|
+
Read the `publishing` topic for publishing work. Local packaging is not publication.
|
|
40
|
+
Obtain clear user confirmation immediately before triggering a GitHub release or
|
|
41
|
+
another external mutation.
|
|
42
|
+
|
|
43
|
+
If the tooling misbehaves, do not work around it in the theme repository: report it at
|
|
44
|
+
https://github.com/dave-atx/nnw-theme/issues with the version line `check` prints.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# NetNewsWire theme format
|
|
2
|
+
|
|
3
|
+
Read this when changing theme structure, metadata, scripts, or resources.
|
|
4
|
+
|
|
5
|
+
The one root-level `<Name>.nnwtheme` directory is the release bundle. It contains
|
|
6
|
+
exactly `Info.plist`, `template.html`, `stylesheet.css`, and optional license or notice
|
|
7
|
+
files. NetNewsWire does not serve arbitrary bundle-local assets: do not add fonts,
|
|
8
|
+
images, media, or references to them. Small demonstrated assets may use data URLs.
|
|
9
|
+
|
|
10
|
+
`Info.plist` requires non-empty `ThemeIdentifier`, `Name`, `CreatorHomePage`, and
|
|
11
|
+
`CreatorName`, plus an integer `Version`. `Name` and the bundle stem match exactly,
|
|
12
|
+
including spaces and capitalization. `CreatorHomePage` is an absolute HTTP(S) URL.
|
|
13
|
+
The identifier and bundle name stay stable after the first release.
|
|
14
|
+
|
|
15
|
+
NetNewsWire performs two single-pass substitutions. First, article values replace
|
|
16
|
+
macros in `template.html`; then that body and combined core/theme CSS enter a platform
|
|
17
|
+
page skeleton. Unknown macros remain literal, and substituted values are not scanned
|
|
18
|
+
again. Common template macros are visible in the starter markup and synthetic
|
|
19
|
+
fixtures.
|
|
20
|
+
|
|
21
|
+
Article text scales through a different mechanism on each platform, and a theme must
|
|
22
|
+
honor both. On macOS the app substitutes `text_size_class` in `template.html` with one
|
|
23
|
+
of `smallText`, `mediumText`, `largeText`, `xLargeText`, or `xxLargeText`, and the
|
|
24
|
+
theme's CSS supplies the sizes; an unset preference means `largeText`, not `mediumText`.
|
|
25
|
+
On iOS and iPadOS the app leaves `text_size_class` unsubstituted and replaces the
|
|
26
|
+
`font-size` macro in the stylesheet with the reader's Dynamic Type body size instead.
|
|
27
|
+
NetNewsWire emits the capital-L `xLargeText` and `xxLargeText` while its bundled themes
|
|
28
|
+
select the lowercase spellings, so define both.
|
|
29
|
+
|
|
30
|
+
Neither rule needs a platform query. A macOS render leaves the `font-size` macro
|
|
31
|
+
literal, exactly as the app does, so that declaration is invalid there and is dropped;
|
|
32
|
+
the size classes are inert on iOS, where the app never sets one. Do not reach for
|
|
33
|
+
`@supports (-webkit-touch-callout: none)` even though NetNewsWire's own stylesheet
|
|
34
|
+
uses it: no desktop WebKit implements that property, so every preview resolves the iOS
|
|
35
|
+
branch as false and platform-conditional CSS is checked the wrong way round. Do not
|
|
36
|
+
write macro names in double brackets inside comments either, because substitution
|
|
37
|
+
rewrites those too. NetNewsWire's stylesheet also sets `font: -apple-system-body` at
|
|
38
|
+
`:root`; a theme with its own typography should not, because that shorthand resets
|
|
39
|
+
`font-family` and `line-height`.
|
|
40
|
+
|
|
41
|
+
The renderer pins NetNewsWire `mac-7.1.3` commit
|
|
42
|
+
`f2daca0f24f29c08d9cb16e1bdc56293fd6fe286`. It uses upstream `core.css`, macOS and
|
|
43
|
+
iOS page skeletons, and `main.js`, `main_mac.js`, `main_ios.js`, and `newsfoot.js`.
|
|
44
|
+
These files ship inside the `nnw-theme` package, verified by SHA-256, so previews work
|
|
45
|
+
offline. iPhone and iPad share the iOS skeleton. WebKit native
|
|
46
|
+
color-scheme emulation controls light/dark captures; CSS is not rewritten.
|
|
47
|
+
|
|
48
|
+
Inline scripts in `template.html` are supported. External scripts and stylesheets are
|
|
49
|
+
rejected. Remote theme-owned fonts, images, audio, or video require an explicit local
|
|
50
|
+
warning override and are still blocked during checks. The article must remain useful
|
|
51
|
+
when Article JavaScript is off; those smoke renders remove theme inline scripts while
|
|
52
|
+
retaining NetNewsWire’s injected scripts.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2002-2025 Brent Simmons
|
|
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.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function scrollDetection() {
|
|
2
|
+
window.onscroll = function(event) {
|
|
3
|
+
window.webkit.messageHandlers.windowDidScroll.postMessage(window.scrollY);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function linkHover() {
|
|
8
|
+
window.onmouseover = function(event) {
|
|
9
|
+
var closestAnchor = event.target.closest('a')
|
|
10
|
+
if (closestAnchor) {
|
|
11
|
+
window.webkit.messageHandlers.mouseDidEnter.postMessage(closestAnchor.href);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
window.onmouseout = function(event) {
|
|
15
|
+
var closestAnchor = event.target.closest('a')
|
|
16
|
+
if (closestAnchor) {
|
|
17
|
+
window.webkit.messageHandlers.mouseDidExit.postMessage(closestAnchor.href);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The selection as an HTML fragment — links, bold, and so on included.
|
|
23
|
+
function selectedHTML() {
|
|
24
|
+
var selection = window.getSelection();
|
|
25
|
+
var container = document.createElement("div");
|
|
26
|
+
for (var i = 0; i < selection.rangeCount; i++) {
|
|
27
|
+
container.appendChild(selection.getRangeAt(i).cloneContents());
|
|
28
|
+
}
|
|
29
|
+
// Relative URLs would break outside this page.
|
|
30
|
+
container.querySelectorAll("a[href]").forEach(function(anchor) {
|
|
31
|
+
anchor.setAttribute("href", anchor.href);
|
|
32
|
+
});
|
|
33
|
+
// A single-paragraph selection doesn't need its <p> wrapper.
|
|
34
|
+
if (container.childNodes.length === 1 && container.firstChild.nodeName === "P") {
|
|
35
|
+
return container.firstChild.innerHTML;
|
|
36
|
+
}
|
|
37
|
+
return container.innerHTML;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function postRenderProcessing() {
|
|
41
|
+
scrollDetection();
|
|
42
|
+
linkHover();
|
|
43
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/* This makes the leading edge of the article stable.
|
|
2
|
+
<https://discourse.netnewswire.com/t/account-for-layout-shift-from-macos-scrollbar-in-themes/328>
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
html {
|
|
6
|
+
scrollbar-gutter: stable;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* Keep absolutely positioned feed content from escaping the body and overlapping the title.
|
|
10
|
+
(.article-body is the Biblioteca theme's name for .articleBody.) */
|
|
11
|
+
|
|
12
|
+
.articleBody,
|
|
13
|
+
.article-body {
|
|
14
|
+
position: relative;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* This is the activity indicator (currently iOS only) that is used while image zooming */
|
|
18
|
+
|
|
19
|
+
.activityIndicatorWrap {
|
|
20
|
+
position: relative;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.activityIndicator {
|
|
24
|
+
z-index: 1;
|
|
25
|
+
width: 64px;
|
|
26
|
+
height: 64px;
|
|
27
|
+
position: absolute;
|
|
28
|
+
top: 50%;
|
|
29
|
+
left: 50%;
|
|
30
|
+
transform: translate(-50%, -50%);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* see removeWpSmiley; this rule is kept in case a wp-smiley is encountered without alt text */
|
|
34
|
+
|
|
35
|
+
.wp-smiley {
|
|
36
|
+
height: 1em !important;
|
|
37
|
+
max-height: 1em !important;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Material for MkDocs and Twemoji render emoji and icons in a .twemoji span or img.
|
|
41
|
+
The inline SVGs have no width or height — without the site's stylesheet
|
|
42
|
+
they'd render at full column width. */
|
|
43
|
+
|
|
44
|
+
.twemoji {
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
height: 1.125em;
|
|
47
|
+
vertical-align: text-top;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.twemoji svg {
|
|
51
|
+
width: 1.125em;
|
|
52
|
+
max-height: 100%;
|
|
53
|
+
fill: currentColor;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
img.twemoji, img.emoji {
|
|
57
|
+
height: 1em !important;
|
|
58
|
+
max-height: 1em !important;
|
|
59
|
+
width: auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Hide the external link at the bottom of Daring Fireball posts */
|
|
63
|
+
|
|
64
|
+
.x-netnewswire-hide {
|
|
65
|
+
display: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*Block ads and junk*/
|
|
69
|
+
|
|
70
|
+
iframe[src*="feedads"],
|
|
71
|
+
iframe[src*="doubleclick"],
|
|
72
|
+
iframe[src*="plusone.google"],
|
|
73
|
+
iframe[src*="slashdot-it.pl"] {
|
|
74
|
+
display: none !important;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
a[href*=".ads."],
|
|
78
|
+
a[href*="feedads"],
|
|
79
|
+
a[href*="doubleclick"],
|
|
80
|
+
a[href*="//ads."],
|
|
81
|
+
a[href*="api.tweetmeme"],
|
|
82
|
+
a[href*="delicious.com/post?"],
|
|
83
|
+
a[href*="digg.com/submit?"],
|
|
84
|
+
a[href*="google.com/bookmarks/mark?"],
|
|
85
|
+
a[href*="posterous.com/share?"],
|
|
86
|
+
a[href*="tumblr.com/share?"],
|
|
87
|
+
a[href*="linkedin.com/shareArticle?"],
|
|
88
|
+
a[href*="facebook.com/share.php?"],
|
|
89
|
+
a[href*="http://twitter.com/home?"],
|
|
90
|
+
a[href*="addtoany.com/share_save"] {
|
|
91
|
+
display: none !important;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
img[src*=".ads."],
|
|
95
|
+
img[src*="//ads."],
|
|
96
|
+
img[src*="doubleclick"],
|
|
97
|
+
img[src*="feedads"],
|
|
98
|
+
img[src*="feedburner"],
|
|
99
|
+
img[src*="feedblitz"],
|
|
100
|
+
img[src*="share-buttons"],
|
|
101
|
+
img[src*="/img/social/"] {
|
|
102
|
+
display: none !important;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.social_items {
|
|
106
|
+
display: none !important;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Mailchimp newsletters end with a follow-our-socials icon wall */
|
|
110
|
+
|
|
111
|
+
.mcnFollowBlock {
|
|
112
|
+
display: none !important;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Newsfoot specific styles. Structural styles come first, theme styles second */
|
|
116
|
+
|
|
117
|
+
.newsfoot-footnote-container {
|
|
118
|
+
position: relative;
|
|
119
|
+
display: inline-block;
|
|
120
|
+
z-index: 9999;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.newsfoot-footnote-popover {
|
|
124
|
+
position: absolute;
|
|
125
|
+
display: block;
|
|
126
|
+
padding: 0em 1em;
|
|
127
|
+
margin: 1em auto;
|
|
128
|
+
top: 0.75em;
|
|
129
|
+
max-width: 44em;
|
|
130
|
+
border-radius: 0.3em;
|
|
131
|
+
box-sizing: border-box;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.newsfoot-footnote-popover {
|
|
135
|
+
left: calc(-1 * (50vw - 1em));
|
|
136
|
+
right: calc(-1 * (50vw - 1em));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.newsfoot-footnote-popover-arrow {
|
|
140
|
+
content: '';
|
|
141
|
+
display: block;
|
|
142
|
+
width: 1em;
|
|
143
|
+
position: absolute;
|
|
144
|
+
top: -0.5em;
|
|
145
|
+
left: calc(50% - 0.5em);
|
|
146
|
+
height: 1em !important;
|
|
147
|
+
transform: rotate(45deg);
|
|
148
|
+
z-index:0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.newsfoot-footnote-popover-inner {
|
|
152
|
+
border-radius: calc(0.3em - 1px);
|
|
153
|
+
padding: 1em;
|
|
154
|
+
position: relative;
|
|
155
|
+
z-index: 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.newsfoot-footnote-popover-inner :first-child {
|
|
159
|
+
margin-top: 0;
|
|
160
|
+
}
|
|
161
|
+
.newsfoot-footnote-popover-inner :last-child {
|
|
162
|
+
margin-bottom: 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.newsfoot-footnote-popover .reversefootnote,
|
|
166
|
+
.newsfoot-footnote-popover .footnoteBackLink,
|
|
167
|
+
.newsfoot-footnote-popover .footnote-return {
|
|
168
|
+
display: none;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
sup[id^='fn'] {
|
|
172
|
+
vertical-align: baseline;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
a.footnote {
|
|
176
|
+
display: inline-block;
|
|
177
|
+
text-decoration: none;
|
|
178
|
+
padding: 0.05em 0.75em;
|
|
179
|
+
border-radius: 1em;
|
|
180
|
+
min-width: 1em;
|
|
181
|
+
text-align: center;
|
|
182
|
+
font-size: 0.8em;
|
|
183
|
+
line-height: 1em;
|
|
184
|
+
position:relative;
|
|
185
|
+
top: -0.1em;
|
|
186
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Here we are making iframes responsive. Particularly useful for inline Youtube videos.
|
|
2
|
+
function wrapFrames() {
|
|
3
|
+
document.querySelectorAll("iframe").forEach(element => {
|
|
4
|
+
if (parseInt(element.height) > 0) {
|
|
5
|
+
preserveAspectRatioOfFixedSizeFrame(element);
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
var wrapper = document.createElement("div");
|
|
9
|
+
wrapper.classList.add("iframeWrap");
|
|
10
|
+
element.parentNode.insertBefore(wrapper, element);
|
|
11
|
+
wrapper.appendChild(element);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// The stylesheet clamps iframes to the article width with max-width: 100%,
|
|
16
|
+
// which leaves an attribute-sized iframe with its full height — distorted,
|
|
17
|
+
// and a huge blank box when the embed fails to load. Let the height follow
|
|
18
|
+
// the width instead.
|
|
19
|
+
function preserveAspectRatioOfFixedSizeFrame(element) {
|
|
20
|
+
const numeric = /^[1-9]\d*$/;
|
|
21
|
+
const width = element.getAttribute("width");
|
|
22
|
+
const height = element.getAttribute("height");
|
|
23
|
+
if (!numeric.test(width) || !numeric.test(height) || element.style.height) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
element.style.aspectRatio = width + " / " + height;
|
|
27
|
+
element.style.height = "auto";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Strip out color and font styling
|
|
31
|
+
|
|
32
|
+
function stripStylesFromElement(element, propertiesToStrip) {
|
|
33
|
+
for (name of propertiesToStrip) {
|
|
34
|
+
element.style.removeProperty(name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Strip inline styles that could harm readability.
|
|
39
|
+
function stripStyles() {
|
|
40
|
+
document.getElementsByTagName("body")[0].querySelectorAll("style, link[rel=stylesheet]").forEach(element => element.remove());
|
|
41
|
+
// Removing "background" and "font" will also remove properties that would be reflected in them, e.g., "background-color" and "font-family"
|
|
42
|
+
document.getElementsByTagName("body")[0].querySelectorAll("[style]").forEach(element => stripStylesFromElement(element, ["color", "background", "font", "max-width", "max-height", "position"]));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Constrain the height of iframes whose heights are defined relative to the document body to be at most
|
|
46
|
+
// 50% of the viewport width.
|
|
47
|
+
function constrainBodyRelativeIframes() {
|
|
48
|
+
let iframes = document.getElementsByTagName("iframe");
|
|
49
|
+
|
|
50
|
+
for (iframe of iframes) {
|
|
51
|
+
// The article body container is positioned, so it — not document.body —
|
|
52
|
+
// is the offsetParent for iframes with no positioned ancestor in the content.
|
|
53
|
+
const offsetParent = iframe.offsetParent;
|
|
54
|
+
if (offsetParent === document.body || (offsetParent && (offsetParent.classList.contains("articleBody") || offsetParent.classList.contains("article-body")))) {
|
|
55
|
+
let heightAttribute = iframe.style.height;
|
|
56
|
+
|
|
57
|
+
if (/%|vw|vh$/i.test(heightAttribute)) {
|
|
58
|
+
iframe.classList.add("nnw-constrained");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Convert all Feedbin proxy images to be used as src, otherwise change image locations to be absolute if not already
|
|
65
|
+
function convertImgSrc() {
|
|
66
|
+
document.querySelectorAll("img").forEach(element => {
|
|
67
|
+
if (element.hasAttribute("data-canonical-src")) {
|
|
68
|
+
element.src = element.getAttribute("data-canonical-src")
|
|
69
|
+
} else if (!/^[a-z]+\:\/\//i.test(element.src)) {
|
|
70
|
+
element.src = new URL(element.src, document.baseURI).href;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Wrap tables in an overflow-x: auto; div
|
|
76
|
+
function wrapTables() {
|
|
77
|
+
var tables = document.querySelectorAll("div.articleBody table");
|
|
78
|
+
|
|
79
|
+
for (table of tables) {
|
|
80
|
+
var wrapper = document.createElement("div");
|
|
81
|
+
wrapper.className = "nnw-overflow";
|
|
82
|
+
table.parentNode.insertBefore(wrapper, table);
|
|
83
|
+
wrapper.appendChild(table);
|
|
84
|
+
|
|
85
|
+
// A negative side margin — usually from the site's own page layout —
|
|
86
|
+
// would put part of the table where the wrapper can't scroll to it.
|
|
87
|
+
if (parseFloat(getComputedStyle(table).marginLeft) < 0) {
|
|
88
|
+
table.style.marginLeft = "0";
|
|
89
|
+
}
|
|
90
|
+
if (parseFloat(getComputedStyle(table).marginRight) < 0) {
|
|
91
|
+
table.style.marginRight = "0";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add the playsinline attribute to any HTML5 videos that don"t have it.
|
|
97
|
+
// Without this attribute videos may autoplay and take over the whole screen
|
|
98
|
+
// on an iphone when viewing an article.
|
|
99
|
+
function inlineVideos() {
|
|
100
|
+
document.querySelectorAll("video").forEach(element => {
|
|
101
|
+
element.setAttribute("playsinline", true);
|
|
102
|
+
if (!element.classList.contains("nnwAnimatedGIF")) {
|
|
103
|
+
element.setAttribute("controls", true);
|
|
104
|
+
element.removeAttribute("autoplay");
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Remove some children (currently just spans) from pre elements to work around a strange clipping issue
|
|
110
|
+
var ElementUnwrapper = {
|
|
111
|
+
unwrapSelector: "span",
|
|
112
|
+
unwrapElement: function (element) {
|
|
113
|
+
var parent = element.parentNode;
|
|
114
|
+
var children = Array.from(element.childNodes);
|
|
115
|
+
|
|
116
|
+
for (child of children) {
|
|
117
|
+
parent.insertBefore(child, element);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
parent.removeChild(element);
|
|
121
|
+
},
|
|
122
|
+
// `elements` can be a selector string, an element, or a list of elements
|
|
123
|
+
unwrapAppropriateChildren: function (elements) {
|
|
124
|
+
if (typeof elements[Symbol.iterator] !== 'function')
|
|
125
|
+
elements = [elements];
|
|
126
|
+
else if (typeof elements === "string")
|
|
127
|
+
elements = document.querySelectorAll(elements);
|
|
128
|
+
|
|
129
|
+
for (element of elements) {
|
|
130
|
+
for (unwrap of element.querySelectorAll(this.unwrapSelector)) {
|
|
131
|
+
this.unwrapElement(unwrap);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
element.normalize()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function flattenPreElements() {
|
|
140
|
+
ElementUnwrapper.unwrapAppropriateChildren("div.articleBody td > pre");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function reloadArticleImage(imageSrc) {
|
|
144
|
+
var image = document.getElementById("nnwImageIcon");
|
|
145
|
+
image.src = imageSrc + "?" + new Date().getTime();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function stopMediaPlayback() {
|
|
149
|
+
document.querySelectorAll("iframe").forEach(element => {
|
|
150
|
+
var iframeSrc = element.src;
|
|
151
|
+
element.src = iframeSrc;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// We pause all videos that have controls. Video without controls shouldn't
|
|
155
|
+
// have sound and are actually converted gifs. Basically if the user can't
|
|
156
|
+
// start the video again, don't stop it.
|
|
157
|
+
document.querySelectorAll("video, audio").forEach(element => {
|
|
158
|
+
if (element.hasAttribute("controls")) {
|
|
159
|
+
element.pause();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function error() {
|
|
165
|
+
document.body.innerHTML = "error";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Takes into account absoluting of URLs.
|
|
169
|
+
function isLocalFootnote(target) {
|
|
170
|
+
return target.hash.startsWith("#fn") && target.href.indexOf(document.baseURI) === 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function styleLocalFootnotes() {
|
|
174
|
+
for (elem of document.querySelectorAll("sup > a[href*='#fn'], sup > div > a[href*='#fn']")) {
|
|
175
|
+
if (isLocalFootnote(elem)) {
|
|
176
|
+
elem.classList.add("footnote");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// convert <img alt="📰" src="[...]" class="wp-smiley"> to a text node containing 📰
|
|
182
|
+
// Tailwind size classes like w-[22px] and h-[22px] encode an SVG icon's intended size.
|
|
183
|
+
// The site stylesheet that implements them is stripped, so apply the sizes directly —
|
|
184
|
+
// otherwise an SVG with no width and height renders at full column width.
|
|
185
|
+
function sizeTailwindSVGs() {
|
|
186
|
+
document.querySelectorAll("div.articleBody svg").forEach(svg => {
|
|
187
|
+
const classAttribute = svg.getAttribute("class") || "";
|
|
188
|
+
const width = classAttribute.match(/(?:^|\s)w-\[([\d.]+(?:px|em|rem))\]/);
|
|
189
|
+
if (width) {
|
|
190
|
+
svg.style.width = width[1];
|
|
191
|
+
}
|
|
192
|
+
const height = classAttribute.match(/(?:^|\s)h-\[([\d.]+(?:px|em|rem))\]/);
|
|
193
|
+
if (height) {
|
|
194
|
+
svg.style.height = height[1];
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function removeWpSmiley() {
|
|
200
|
+
for (const img of document.querySelectorAll("img.wp-smiley[alt]")) {
|
|
201
|
+
img.parentNode.replaceChild(document.createTextNode(img.alt), img);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function processPage() {
|
|
206
|
+
wrapFrames();
|
|
207
|
+
wrapTables();
|
|
208
|
+
inlineVideos();
|
|
209
|
+
stripStyles();
|
|
210
|
+
sizeTailwindSVGs();
|
|
211
|
+
constrainBodyRelativeIframes();
|
|
212
|
+
convertImgSrc();
|
|
213
|
+
flattenPreElements();
|
|
214
|
+
styleLocalFootnotes();
|
|
215
|
+
removeWpSmiley()
|
|
216
|
+
postRenderProcessing();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
document.addEventListener("DOMContentLoaded", function(event) {
|
|
220
|
+
processPage();
|
|
221
|
+
})
|