File: htmlview.js

package info (click to toggle)
liferea 1.15.8-2
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 7,652 kB
  • sloc: ansic: 27,425; javascript: 1,646; python: 1,123; makefile: 657; sh: 115; xml: 13
file content (205 lines) | stat: -rw-r--r-- 7,068 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// vim: set ts=4 sw=4:
/*
 * @file htmlview.c  htmlview reader mode switching and CSS handling
 *
 * Copyright (C) 2021-2023 Lars Windolf <lars.windolf@gmx.de>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

function setBase(uri) {
	var base = document.createElement("base");
	base.setAttribute("href", uri);
	document.head.appendChild(base);
}

/**
 * loadContent() will be run on each internal / Readability.js rendering
 *
 * This method can be called multiple times for a single rendering, so it
 * needs to be idempotent on what is done. Primary use case for this is
 * when in internal browser + reader mode we first render layout with parameter
 * content being empty, while asynchronously downloading content. Once the
 * download finishes loadContent() is called again with the actual content
 * which is then inserted in the layout.
 *
 * @returns: false if loading with reader failed (true otherwise)
 */
function loadContent(readerEnabled, content) {
	var internalBrowsing = false;

	if (false == readerEnabled) {
		if (document.location.href === 'liferea://') {
			console.log('[liferea] reader mode is off');
			document.body.innerHTML = decodeURIComponent(content);
		} else {
			console.log('[liferea] reader mode off for website');
		}
	}
	if (true == readerEnabled) {
		try {
			console.log('[liferea] reader mode is on');
			var documentClone = document.cloneNode(true);

			// When we are internally browsing than we need basic
			// structure to insert Reader mode content
			if (document.getElementById('content') !== null) {
				internalBrowsing = true;
				console.log('[liferea] adding <div id="content"> for website content');
				document.body.innerHTML += '<div id=\"content\"></div>';
			}

			// Decide where we get the content from
			if (document.location.href === 'liferea://') {
				// Add all content in shadow DOM and split decoration from content
				// only pass the content to Readability.js
				console.log('[liferea] load content passed by variable');
				content = decodeURIComponent(content);
			} else {
				console.log('[liferea] using content from original document');
				content = document.documentElement.innerHTML;
			}

			// Add content to clone doc as input for Readability.js
			documentClone.body.innerHTML = content;

			// When we run with internal URI schema we get layout AND content
			// from variable and split it, apply layout to document
			// and copy content to documentClone
			if (document.location.href === 'liferea://' && documentClone.getElementById('content') != null) {
				documentClone.getElementById('content').innerHTML = '';
				document.body.innerHTML = documentClone.body.innerHTML;
				documentClone.body.innerHTML = content;
				documentClone.body.innerHTML = documentClone.getElementById('content').innerHTML;
			}

			try {
				if (!isProbablyReaderable(documentClone))
					throw "notreaderable";

				// Show the results
				// Kill all foreign styles
				var links = document.querySelectorAll('link');
				for (var l of links) {
					l.parentNode.removeChild(l);
				}
				var styles = document.querySelectorAll('style');
				for (var s of styles) {
					s.parentNode.removeChild(s);
				}

				var article = new Readability(documentClone, {
					charThreshold: 25
				}).parse();

				if (!article)
					throw "noarticle";

				document.getElementById('content').innerHTML = article.content;

			} catch (e) {
				console.log('[liferea] reader mode not possible (' + e + ')! fallback to unfiltered content');
				if (internalBrowsing)
					document.getElementById('content').innerHTML = "Reader mode not possible. Loading URL unfiltered...";	// FIXME: provide good error info
				else
					document.body.innerHTML = content;
				return false;
			}

			// Kill all foreign styles
			var links = document.querySelectorAll('link');
			for (var l of links) {
				l.parentNode.removeChild(l);
			}
			var styles = document.querySelectorAll('style');
			for (var s of styles) {
				s.parentNode.removeChild(s);
			}
		} catch (e) {
			console.log('[liferea] reader mode failed: ' + e);
			if (!internalBrowsing) {
				// Force load original document at top level to get rid of all decoration
				document.documentElement.innerHTML = content;
			} else {
				document.getElementById('content').innerHTML = "Reader mode failed. Loading URL unfiltered...";
				return false;
			}
		}
	}

	// Run DOMPurify
	content = document.getElementById('content').innerHTML;
	document.getElementById('content').innerHTML = DOMPurify.sanitize(content);

	// Fix inline SVG sizes
	const svgMinWidth = 50;
	document.getElementById('content')
		.querySelectorAll('svg')
		.forEach((el) => {
			const h = el.getAttribute('height');
			const w = el.getAttribute('width');
			if(h && w) {
				if(w < svgMinWidth)
					el.parentNode.removeChild(el);
				return;
			}

			const viewbox = el.getAttribute('viewBox');
			if(!viewbox)
				return;

			const size = viewbox.split(/\s+/);
			if(size.length != 4)
				return;

			// Drop smaller SVGs that are usually just layout decorations
			if((size[2] - size[0]) < svgMinWidth) {
				el.parentNode.removeChild(el);
				return;
			}

			// Properly size larger SVGs
			el.width = size[2] - size[0];
			el.heigth = size[3] - size[1];
		});

	// Drop empty elements (to get rid of empty picture/video/iframe divs)
	const emptyRegex = new RegExp("^\s*$");
	document.getElementById('content')
		.querySelectorAll(":only-child")
		.forEach((el) => {
			if(el.innerHTML.length == 1)
				el.parentNode.removeChild(el);
		});

    // Setup audio/video <select> handler
    document.querySelector('#enclosureVideo select')?.addEventListener("change", (e) => {
		document.querySelector('#enclosureVideo video').src = e.target.options[e.target.selectedIndex].value;
		document.querySelector('#enclosureVideo video').play();
    });
    document.getElementById('#enclosureAudio select')?.addEventListener("change", (e) => {
		document.querySelector('#enclosureAudio audio').src = e.target.options[e.target.selectedIndex].value;
		document.querySelector('#enclosureVideo audio').play();
    });

	return true;
}

function youtube_embed(id) {
	var container = document.getElementById(id);
	container.innerHTML = '<iframe width="640" height="480" src="https://www.youtube.com/embed/' + id + '?autoplay=1" frameborder="0" allowfullscreen="1" allow="autoplay; allowfullscreen"></iframe>';

	return false;
}