mirror of
https://github.com/nkanaev/yarr.git
synced 2026-06-24 17:15:17 +00:00
Compare commits
4 Commits
ce1c4863ee
...
a18ed04193
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a18ed04193 | ||
|
|
31f2ca57df | ||
|
|
d0f8e70095 | ||
|
|
af7a38fccd |
@@ -4,6 +4,7 @@
|
|||||||
- (fix) crash on empty article list with article is selected (thanks to @rksvc)
|
- (fix) crash on empty article list with article is selected (thanks to @rksvc)
|
||||||
- (fix) invalid article title in RSS feeds with media containing titles (thanks to @bwwu-git for the report)
|
- (fix) invalid article title in RSS feeds with media containing titles (thanks to @bwwu-git for the report)
|
||||||
- (fix) missing image enclosures in certain RSS feeds (thanks to @palinek for the report)
|
- (fix) missing image enclosures in certain RSS feeds (thanks to @palinek for the report)
|
||||||
|
- (fix) parsing namespaced legacy RSS feeds (thanks to @f100024)
|
||||||
|
|
||||||
# v2.6 (2025-11-24)
|
# v2.6 (2025-11-24)
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
<div class="row text-center m-0">
|
<div class="row text-center m-0">
|
||||||
<button class="btn btn-link col-4 px-0 rounded-0"
|
<button class="btn btn-link col-4 px-0 rounded-0"
|
||||||
:class="'theme-'+t"
|
:class="'theme-'+t"
|
||||||
|
:title="t"
|
||||||
:aria-label="t"
|
:aria-label="t"
|
||||||
:aria-pressed="theme.name == t"
|
:aria-pressed="theme.name == t"
|
||||||
@click.stop="theme.name = t"
|
@click.stop="theme.name = t"
|
||||||
@@ -126,13 +127,17 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<header class="dropdown-header" role="heading" aria-level="2">ᚨ / 𐎠 / 𑖀</header>
|
<header class="dropdown-header" role="heading" aria-level="2">ᚨ / 𐎠 / 𑖀</header>
|
||||||
<button
|
<div class="d-flex">
|
||||||
v-for="lang in languages"
|
<button
|
||||||
class="dropdown-item"
|
v-for="lang in languages"
|
||||||
:class="{active: language==lang.code}"
|
class="dropdown-item text-center"
|
||||||
@click.stop="changeLanguage(lang.code)">
|
:aria-label="lang.name"
|
||||||
{{ lang.name }}
|
:title="lang.name"
|
||||||
</button>
|
:class="{active: language==lang.code}"
|
||||||
|
@click.stop="changeLanguage(lang.code)">
|
||||||
|
{{ lang.code }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="dropdown-divider" v-if="authenticated"></div>
|
<div class="dropdown-divider" v-if="authenticated"></div>
|
||||||
<button class="dropdown-item" v-if="authenticated" @click="logout()">
|
<button class="dropdown-item" v-if="authenticated" @click="logout()">
|
||||||
<span class="icon mr-1">{% inline "log-out.svg" %}</span>
|
<span class="icon mr-1">{% inline "log-out.svg" %}</span>
|
||||||
|
|||||||
@@ -4,9 +4,6 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 15px !important;
|
font-size: 15px !important;
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func TestAtomHTMLTitle(t *testing.T) {
|
|||||||
feed, _ := Parse(strings.NewReader(`
|
feed, _ := Parse(strings.NewReader(`
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
<entry><title type="html">say <code>what</code>?</entry>
|
<entry><title type="html">say <code>what</code>?</title></entry>
|
||||||
</feed>
|
</feed>
|
||||||
`))
|
`))
|
||||||
have := feed.Items[0].Title
|
have := feed.Items[0].Title
|
||||||
@@ -96,12 +96,13 @@ func TestAtomXHTMLTitle(t *testing.T) {
|
|||||||
feed, _ := Parse(strings.NewReader(`
|
feed, _ := Parse(strings.NewReader(`
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
<entry><title type="xhtml">say <code>what</code>?</entry>
|
<entry><title type="xhtml">say <code>what</code>?</title></entry>
|
||||||
</feed>
|
</feed>
|
||||||
`))
|
`))
|
||||||
have := feed.Items[0].Title
|
have := feed.Items[0].Title
|
||||||
want := "say what?"
|
want := "say what?"
|
||||||
if !reflect.DeepEqual(want, have) {
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Log(feed)
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
t.Logf("have: %#v", have)
|
t.Logf("have: %#v", have)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
|||||||
@@ -48,12 +48,6 @@ type rssLink struct {
|
|||||||
Rel string `xml:"rel,attr"`
|
Rel string `xml:"rel,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type rssTitle struct {
|
|
||||||
XMLName xml.Name
|
|
||||||
Data string `xml:",chardata"`
|
|
||||||
Inner string `xml:",innerxml"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type rssEnclosure struct {
|
type rssEnclosure struct {
|
||||||
URL string `xml:"url,attr"`
|
URL string `xml:"url,attr"`
|
||||||
Type string `xml:"type,attr"`
|
Type string `xml:"type,attr"`
|
||||||
@@ -63,9 +57,10 @@ type rssEnclosure struct {
|
|||||||
func ParseRSS(r io.Reader) (*Feed, error) {
|
func ParseRSS(r io.Reader) (*Feed, error) {
|
||||||
srcfeed := rssFeed{}
|
srcfeed := rssFeed{}
|
||||||
|
|
||||||
decoder := xmlDecoder(r)
|
rawDecoder := xmlDecoder(r)
|
||||||
decoder.DefaultSpace = "rss"
|
rawDecoder.DefaultSpace = "rss"
|
||||||
if err := decoder.Decode(&srcfeed); err != nil {
|
rssDecoder := xml.NewTokenDecoder(&rssTokenReader{Decoder: rawDecoder})
|
||||||
|
if err := rssDecoder.Decode(&srcfeed); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -327,3 +327,68 @@ func TestRSSMultipleMedia(t *testing.T) {
|
|||||||
t.Fatal("invalid rss")
|
t.Fatal("invalid rss")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When both RSS <link> and Atom <atom:link> elements are present in an item,
|
||||||
|
// the RSS link must not be lost. The <link> tag is namespace-qualified as
|
||||||
|
// `rss link` to disambiguate — see commit ee2a825, found in:
|
||||||
|
// https://rss.nytimes.com/services/xml/rss/nyt/Arts.xml
|
||||||
|
func TestRSSItemLinkWithAtomLinkPresent(t *testing.T) {
|
||||||
|
have, _ := Parse(strings.NewReader(`
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<title>Example</title>
|
||||||
|
<item>
|
||||||
|
<title>Article</title>
|
||||||
|
<link>http://example.com/article/1</link>
|
||||||
|
<atom:link href="http://example.com/article/1/atom" rel="alternate" type="text/html"/>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
`))
|
||||||
|
want := &Feed{
|
||||||
|
Title: "Example",
|
||||||
|
Items: []Item{
|
||||||
|
{
|
||||||
|
GUID: "http://example.com/article/1",
|
||||||
|
URL: "http://example.com/article/1",
|
||||||
|
Title: "Article",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Fatalf("RSS link lost when atom:link is present\nwant: %#v\nhave: %#v", want, have)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feeds that declare a default namespace on the root <rss> element (e.g. the
|
||||||
|
// legacy Userland namespace) must still parse — see sud.ua/rss/rss_news_uk.xml.
|
||||||
|
func TestRSSDefaultNamespace(t *testing.T) {
|
||||||
|
have, _ := Parse(strings.NewReader(`
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rss xmlns="http://backend.userland.com/rss2" version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>Feed</title>
|
||||||
|
<item>
|
||||||
|
<title>Title 1</title>
|
||||||
|
<link>https://example.com/news/1</link>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
`))
|
||||||
|
want := &Feed{
|
||||||
|
Title: "Feed",
|
||||||
|
Items: []Item{
|
||||||
|
{
|
||||||
|
GUID: "https://example.com/news/1",
|
||||||
|
URL: "https://example.com/news/1",
|
||||||
|
Title: "Title 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Fatalf("default-namespaced rss not parsed \nwant: %#v\nhave: %#v", want, have)
|
||||||
|
// t.Logf("have: %#v", have)
|
||||||
|
// t.Fatal("default-namespaced rss not parsed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,59 @@ func xmlDecoder(r io.Reader) *xml.Decoder {
|
|||||||
return decoder
|
return decoder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XML token reader that strips the default namespace.
|
||||||
|
// It's primary purpose is to support namespaced legacy UserLand RSS feeds.
|
||||||
|
// NOTE: token readers cannot populate ",innerxml"-tagged struct fields,
|
||||||
|
// see https://github.com/golang/go/issues/39645
|
||||||
|
type rssTokenReader struct {
|
||||||
|
Decoder *xml.Decoder
|
||||||
|
defaultNS string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rssTokenReader) Token() (xml.Token, error) {
|
||||||
|
tok, err := r.Decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t := tok.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
// extract default namespace: <rss xmlns="<defaultNS>">
|
||||||
|
if t.Name.Local == "rss" {
|
||||||
|
for _, attr := range t.Attr {
|
||||||
|
if attr.Name.Space == "" && attr.Name.Local == "xmlns" && attr.Value != "" {
|
||||||
|
r.defaultNS = attr.Value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.defaultNS != "" {
|
||||||
|
// Rewrite element namespace
|
||||||
|
if t.Name.Space == r.defaultNS {
|
||||||
|
t.Name.Space = r.Decoder.DefaultSpace
|
||||||
|
}
|
||||||
|
// Rewrite attribute namespaces
|
||||||
|
attrs := t.Attr[:0]
|
||||||
|
for _, a := range t.Attr {
|
||||||
|
if a.Name.Space == r.defaultNS {
|
||||||
|
a.Name.Space = r.Decoder.DefaultSpace
|
||||||
|
}
|
||||||
|
attrs = append(attrs, a)
|
||||||
|
}
|
||||||
|
t.Attr = attrs
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
case xml.EndElement:
|
||||||
|
if r.defaultNS != "" && t.Name.Space == r.defaultNS {
|
||||||
|
t.Name.Space = r.Decoder.DefaultSpace
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
default:
|
||||||
|
return tok, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type safexmlreader struct {
|
type safexmlreader struct {
|
||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
buffer *bytes.Buffer
|
buffer *bytes.Buffer
|
||||||
|
|||||||
Reference in New Issue
Block a user