mirror of
https://github.com/nkanaev/yarr.git
synced 2026-06-24 00:55:16 +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) 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) parsing namespaced legacy RSS feeds (thanks to @f100024)
|
||||
|
||||
# v2.6 (2025-11-24)
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
<div class="row text-center m-0">
|
||||
<button class="btn btn-link col-4 px-0 rounded-0"
|
||||
:class="'theme-'+t"
|
||||
:title="t"
|
||||
:aria-label="t"
|
||||
:aria-pressed="theme.name == t"
|
||||
@click.stop="theme.name = t"
|
||||
@@ -126,13 +127,17 @@
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<header class="dropdown-header" role="heading" aria-level="2">ᚨ / 𐎠 / 𑖀</header>
|
||||
<button
|
||||
v-for="lang in languages"
|
||||
class="dropdown-item"
|
||||
:class="{active: language==lang.code}"
|
||||
@click.stop="changeLanguage(lang.code)">
|
||||
{{ lang.name }}
|
||||
</button>
|
||||
<div class="d-flex">
|
||||
<button
|
||||
v-for="lang in languages"
|
||||
class="dropdown-item text-center"
|
||||
:aria-label="lang.name"
|
||||
:title="lang.name"
|
||||
:class="{active: language==lang.code}"
|
||||
@click.stop="changeLanguage(lang.code)">
|
||||
{{ lang.code }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-divider" v-if="authenticated"></div>
|
||||
<button class="dropdown-item" v-if="authenticated" @click="logout()">
|
||||
<span class="icon mr-1">{% inline "log-out.svg" %}</span>
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
|
||||
html {
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
body {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestAtomHTMLTitle(t *testing.T) {
|
||||
feed, _ := Parse(strings.NewReader(`
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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>
|
||||
`))
|
||||
have := feed.Items[0].Title
|
||||
@@ -96,12 +96,13 @@ func TestAtomXHTMLTitle(t *testing.T) {
|
||||
feed, _ := Parse(strings.NewReader(`
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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>
|
||||
`))
|
||||
have := feed.Items[0].Title
|
||||
want := "say what?"
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Log(feed)
|
||||
t.Logf("want: %#v", want)
|
||||
t.Logf("have: %#v", have)
|
||||
t.FailNow()
|
||||
|
||||
@@ -48,12 +48,6 @@ type rssLink struct {
|
||||
Rel string `xml:"rel,attr"`
|
||||
}
|
||||
|
||||
type rssTitle struct {
|
||||
XMLName xml.Name
|
||||
Data string `xml:",chardata"`
|
||||
Inner string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type rssEnclosure struct {
|
||||
URL string `xml:"url,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
@@ -63,9 +57,10 @@ type rssEnclosure struct {
|
||||
func ParseRSS(r io.Reader) (*Feed, error) {
|
||||
srcfeed := rssFeed{}
|
||||
|
||||
decoder := xmlDecoder(r)
|
||||
decoder.DefaultSpace = "rss"
|
||||
if err := decoder.Decode(&srcfeed); err != nil {
|
||||
rawDecoder := xmlDecoder(r)
|
||||
rawDecoder.DefaultSpace = "rss"
|
||||
rssDecoder := xml.NewTokenDecoder(&rssTokenReader{Decoder: rawDecoder})
|
||||
if err := rssDecoder.Decode(&srcfeed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -327,3 +327,68 @@ func TestRSSMultipleMedia(t *testing.T) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
reader *bufio.Reader
|
||||
buffer *bytes.Buffer
|
||||
|
||||
Reference in New Issue
Block a user