card view, minor css bugfixes
This commit is contained in:
@@ -118,6 +118,34 @@ describe('useFeeds', () => {
|
||||
expect(feeds.value[0].content).not.toMatch(/src="[^"]*(\$\{|%7[bB])/)
|
||||
})
|
||||
|
||||
it('resolves templated images regardless of the placeholder name and scrubs other lazy-load attributes', async () => {
|
||||
feeds.value = [{
|
||||
id: 1,
|
||||
title: 'Article one',
|
||||
url: 'https://www.dw.com/en/article-one/a-1',
|
||||
content: '',
|
||||
}]
|
||||
axios.post.mockResolvedValueOnce({
|
||||
data: {
|
||||
content: `<html><body><article>
|
||||
<img data-format="MASTER_LANDSCAPE" data-id="76212061"
|
||||
data-url="https://static.dw.com/image/76212061_\${size}.jpg"
|
||||
data-src="https://static.dw.com/image/76212061_\${size}.jpg"
|
||||
alt="Merz and Trump" src="data:image/gif;base64,placeholder">
|
||||
<p>some article text long enough for readability to keep the image and paragraph together in the parsed output, padded with extra words to pass the content-length heuristics used by Mozilla Readability when scoring candidate nodes for the main article body.</p>
|
||||
</article></body></html>`,
|
||||
},
|
||||
})
|
||||
|
||||
await getReadable(feeds.value[0], 0)
|
||||
|
||||
// `${size}` isn't the hardcoded `${formatId}` placeholder, but `data-format`
|
||||
// is still the right substitution — and lazy-load attributes like `data-src`
|
||||
// (which Readability may promote into `src`) must be cleaned too, not just
|
||||
// `data-url`, so a stale template can't resurface in the rendered output.
|
||||
expect(feeds.value[0].content).not.toMatch(/src="[^"]*(\$\{|%7[bB])/)
|
||||
})
|
||||
|
||||
it('drops unresolvable templated images instead of leaving a broken src', async () => {
|
||||
feeds.value = [{
|
||||
id: 1,
|
||||
|
||||
@@ -10,6 +10,7 @@ const message = ref('')
|
||||
const showModal = ref(false)
|
||||
const viewMode = ref('list') // 'list' | 'article' — toggled from the hamburger menu
|
||||
const currentIndex = ref(0)
|
||||
const layout = ref('list') // 'list' | 'cards' — list-view display style, toggled from the hamburger menu
|
||||
|
||||
let observer; // Declare observer outside the setup function
|
||||
let initialLoad = false
|
||||
@@ -23,18 +24,33 @@ function authHeaders() {
|
||||
}
|
||||
}
|
||||
|
||||
// Some feeds (e.g. Deutsche Welle) ship <img> tags whose `src`/`data-url`
|
||||
// contain an unresolved `${formatId}` template that their own frontend fills
|
||||
// in from the sibling `data-format` attribute before loading — verbatim they
|
||||
// 404. Resolve them the same way here, or drop the <img> if we can't, so
|
||||
// Readability doesn't carry a broken image into the parsed article.
|
||||
// Some feeds (e.g. Deutsche Welle) ship <img> tags whose `src` and various
|
||||
// lazy-load attributes (`data-url`, `data-src`, `srcset`, ...) contain an
|
||||
// unresolved `${placeholderName}` template — or its URL-encoded `%7B...%7D`
|
||||
// form — that their own frontend fills in from the sibling `data-format`
|
||||
// attribute before loading; verbatim they 404. Resolve every such attribute
|
||||
// the same way (so Readability's own lazy-image handling can't resurrect a
|
||||
// stale template into `src`), preferring `data-url` as the source of truth
|
||||
// for `src`, and drop the <img> entirely if a template still remains.
|
||||
const TEMPLATE_PATTERN = /\$\{[^}]+\}|%7[bB][^%]*%7[dD]/
|
||||
const TEMPLATE_PATTERN_GLOBAL = /\$\{[^}]+\}|%7[bB][^%]*%7[dD]/g
|
||||
|
||||
function resolveTemplatedImage(img) {
|
||||
const placeholder = '${formatId}'
|
||||
const format = img.getAttribute('data-format')
|
||||
const dataUrl = img.getAttribute('data-url')
|
||||
if (format && dataUrl && dataUrl.includes(placeholder)) {
|
||||
img.setAttribute('src', dataUrl.replace(placeholder, format))
|
||||
} else if (/[{]|%7[bB]/.test(img.getAttribute('src') ?? '')) {
|
||||
|
||||
if (format) {
|
||||
if (dataUrl && TEMPLATE_PATTERN.test(dataUrl)) {
|
||||
img.setAttribute('src', dataUrl.replace(TEMPLATE_PATTERN_GLOBAL, format))
|
||||
}
|
||||
for (const attr of [...img.attributes]) {
|
||||
if (attr.name !== 'src' && TEMPLATE_PATTERN.test(attr.value)) {
|
||||
img.setAttribute(attr.name, attr.value.replace(TEMPLATE_PATTERN_GLOBAL, format))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TEMPLATE_PATTERN.test(img.getAttribute('src') ?? '')) {
|
||||
img.remove()
|
||||
}
|
||||
}
|
||||
@@ -175,17 +191,36 @@ function markCurrentArticleRead() {
|
||||
const feed = feeds.value[currentIndex.value]
|
||||
// Marking read here (rather than via removeFeed, as the scroll-based list
|
||||
// view does) keeps the array stable so currentIndex stays valid while paging.
|
||||
if (feed) markRead(feed.id)
|
||||
// The local `read` flag lets leaveArticleView() drop these once we're done.
|
||||
if (feed) {
|
||||
feed.read = true
|
||||
markRead(feed.id)
|
||||
}
|
||||
}
|
||||
|
||||
function leaveArticleView() {
|
||||
// Articles paged past in article view were marked read but deliberately kept
|
||||
// in place so currentIndex stayed valid — drop them now so they don't keep
|
||||
// showing up in the list view.
|
||||
feeds.value = feeds.value.filter(feed => !feed.read)
|
||||
currentIndex.value = 0
|
||||
viewMode.value = 'list'
|
||||
}
|
||||
|
||||
function toggleViewMode() {
|
||||
viewMode.value = viewMode.value === 'list' ? 'article' : 'list'
|
||||
if (viewMode.value === 'article') {
|
||||
leaveArticleView()
|
||||
} else {
|
||||
viewMode.value = 'article'
|
||||
currentIndex.value = 0
|
||||
markCurrentArticleRead()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLayout() {
|
||||
layout.value = layout.value === 'list' ? 'cards' : 'list'
|
||||
}
|
||||
|
||||
function nextArticle() {
|
||||
if (currentIndex.value < feeds.value.length - 1) {
|
||||
currentIndex.value += 1
|
||||
@@ -209,6 +244,9 @@ export function useFeeds() {
|
||||
viewMode,
|
||||
currentIndex,
|
||||
toggleViewMode,
|
||||
leaveArticleView,
|
||||
layout,
|
||||
toggleLayout,
|
||||
nextArticle,
|
||||
prevArticle,
|
||||
fetchData,
|
||||
|
||||
Reference in New Issue
Block a user