Compare commits
37 commits
apt-instru
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
68db51ea94 | ||
|
5fd6a6c710 | ||
|
2cdba527f6 | ||
3724eaa0a8 | |||
|
3ac005708d | ||
|
766e51af4c | ||
|
84e54085b5 | ||
|
74b0f50c61 | ||
|
028fdacc39 | ||
|
b26958acae | ||
|
b5762e90ef | ||
|
ad34af72ff | ||
|
44e7778578 | ||
|
a9c7832da1 | ||
|
8ab7396a36 | ||
|
cb4507bb78 | ||
|
040d532fb9 | ||
|
a3c989cec9 | ||
|
1138faceda | ||
|
eb280b5aab | ||
|
694c8bb489 | ||
|
d1f1edbaca | ||
|
e07ff6e838 | ||
|
16bd475be2 | ||
ed33aca596 | |||
|
a1e4cf0f71 | ||
a4c7ada540 | |||
af7f31e19b | |||
d0ded117e2 | |||
7f6be68df1 | |||
45e484a409 | |||
7f26996860 | |||
ea51e8198e | |||
2a42e7b202 | |||
|
11ee189748 | ||
|
7712b7d9b8 | ||
|
0785b167ad |
167
README.md
|
@ -1,82 +1,136 @@
|
|||
# XMPP PubSub To Atom
|
||||
# Rivista XJP
|
||||
|
||||
A little client that parses XMPP Pubsub Nodes and sends them as Atom Syndication Format or OPML over HTTP.
|
||||
Rivista XJP ("XMPP Journal Publisher"); previously XMPP PubSub To Atom ("XPTA").
|
||||
|
||||
## About
|
||||
Rivista is a software that parses XMPP Pubsub Nodes and sends them as Atom
|
||||
Syndication Format or OPML over HTTP.
|
||||
|
||||
XMPP PubSub To Atom ("XPTA") is a simple Python script that parses XMPP Pubsub Nodes and sends them as Atom Syndication Format or OPML over HTTP.
|
||||
Rivista generates Atom syndication feeds
|
||||
([RFC 4287](https://www.rfc-editor.org/rfc/rfc4287)) from XMPP PubSub nodes
|
||||
([XEP-0060](http://xmpp.org/extensions/xep-0060.html)).
|
||||
|
||||
XPTA generates Atom syndication feeds ([RFC 4287](https://www.rfc-editor.org/rfc/rfc4287)) from XMPP PubSub nodes ([XEP-0060](http://xmpp.org/extensions/xep-0060.html)).
|
||||
Rivista includes [XSLT ](https://www.w3.org/TR/xslt/) stylesheets that transform
|
||||
PubSub nodes into static XHTML journal sites.
|
||||
|
||||
This software was inspired from Tigase and was motivated by Movim.
|
||||
Rivista was inspired from Tigase and was motivated by Movim.
|
||||
|
||||
## Home site
|
||||
|
||||
- gemini://woodpeckersnest.space/~schapps/rivista.gmi
|
||||
- https://schapps.woodpeckersnest.eu/rivista/
|
||||
|
||||
## Instances
|
||||
|
||||
* https://rivista.woodpeckersnest.eu
|
||||
|
||||
## Preview
|
||||
|
||||
[<img alt="berlin-xmpp-meetup" src="screenshot/berlin-xmpp-meetup.png" width="200px"/>](screenshot/berlin-xmpp-meetup.png)
|
||||
[<img alt="let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh" src="screenshot/let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh.png" width="200px"/>](screenshot/let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh.png)
|
||||
[<img alt="59d860ab-d7c8-477c-bb4b-86924485cbbb" src="screenshot/59d860ab-d7c8-477c-bb4b-86924485cbbb.png" width="200px"/>](screenshot/59d860ab-d7c8-477c-bb4b-86924485cbbb.png)
|
||||
[<img alt="selection" src="screenshot/selection.png" width="200px"/>](screenshot/selection.png)
|
||||
[<img alt="berlin-xmpp-meetup" src="rivista/screenshot/berlin-xmpp-meetup.png" width="200px"/>](rivista/screenshot/berlin-xmpp-meetup.png)
|
||||
[<img alt="let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh" src="rivista/screenshot/let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh.png" width="200px"/>](rivista/screenshot/let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh.png)
|
||||
[<img alt="59d860ab-d7c8-477c-bb4b-86924485cbbb" src="rivista/screenshot/59d860ab-d7c8-477c-bb4b-86924485cbbb.png" width="200px"/>](rivista/screenshot/59d860ab-d7c8-477c-bb4b-86924485cbbb.png)
|
||||
[<img alt="selection" src="rivista/screenshot/selection.png" width="200px"/>](rivista/screenshot/selection.png)
|
||||
|
||||
## Motivation
|
||||
|
||||
Rivista is a syndication project which makes journals and publications that are
|
||||
hosted on XMPP PubSub nodes, available from HTTP to both, XML news readers and
|
||||
even HTML browsers.
|
||||
|
||||
This means that instead of hosting a journal or publication site in the old
|
||||
fashion (i.e. HTML documents hosted on an HTTP server), one only has to have an
|
||||
HTTP server to operate Rivista, and the rest of the content is delivered from an
|
||||
XMPP server (i.e. PubSub nodes).
|
||||
|
||||
The project also showcases the non-necessity of HTML, as it automatically
|
||||
generates valid XHTML pages by HTML browsers (client-side) from XSLT
|
||||
stylesheets.
|
||||
|
||||
Because Rivista reads XMPP PubSub nodes, it is possible to view a complete set
|
||||
of node items, and even a single node item, which means, that a considered and
|
||||
carefully earnest use of Rivista would save bandwidth and system overhead, which
|
||||
includes CPU, I/O and RAM usage.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Python >= 3.5
|
||||
* beautifulsoup4
|
||||
* fastapi
|
||||
* feedgenerator
|
||||
* lxml
|
||||
* markdown
|
||||
* slixmpp
|
||||
* tomllib
|
||||
* tomllib (Python <= 3.10)
|
||||
* uvicorn
|
||||
|
||||
## Installation
|
||||
|
||||
### Debian-based Distro
|
||||
It is possible to install Rivista using pip and pipx.
|
||||
|
||||
```shell
|
||||
# apt install python3 python3-fastapi python3-feedgenerator python3-lxml python3-slixmpp python3-tomli uvicorn
|
||||
#### pip inside venv
|
||||
|
||||
```
|
||||
$ python3 -m venv .venv
|
||||
$ source .venv/bin/activate
|
||||
```
|
||||
|
||||
### Download
|
||||
##### Install
|
||||
|
||||
Extract the source package to a directory that you have permission to run
|
||||
software.
|
||||
|
||||
```shell
|
||||
$ git clone https://git.xmpp-it.net/sch/PubSubToAtom
|
||||
$ cd PubSubToAtom/
|
||||
```
|
||||
$ pip install git+https://git.xmpp-it.net/sch/Rivista
|
||||
```
|
||||
|
||||
### Configure
|
||||
#### pipx
|
||||
|
||||
Add account credentials to file `configuration.toml`.
|
||||
##### Install
|
||||
|
||||
```
|
||||
$ pipx install git+https://git.xmpp-it.net/sch/Rivista
|
||||
```
|
||||
|
||||
##### Update
|
||||
|
||||
```
|
||||
$ pipx uninstall rivista
|
||||
$ pipx install git+https://git.xmpp-it.net/sch/Rivista
|
||||
```
|
||||
|
||||
### Start
|
||||
|
||||
Execute PubSubToAtom with one of the following commands:
|
||||
|
||||
```shell
|
||||
$ python -m uvicorn pubsub_to_atom:app --reload
|
||||
$ python -m uvicorn pubsub_to_atom:app --reload --host 127.0.0.1 --port 8000
|
||||
$ uvicorn pubsub_to_atom:app --host 127.0.0.1 --port 8000
|
||||
$ fastapi dev pubsub_to_atom.py
|
||||
```
|
||||
$ rivista
|
||||
```
|
||||
|
||||
Open URL http://localhost:8000
|
||||
|
||||
## Usage
|
||||
|
||||
It is possible to view a complete node and even a single item, which means, that it is possible to save bandwidth and it further means that a considered and carefully earnest use of this software would saves system overhead, which includes CPU, I/O and RAM usage.
|
||||
It is possible to view a complete node and even a single item, which means, that
|
||||
it is possible to save bandwidth and it further means that a considered and
|
||||
carefully earnest use of this software would save system overhead, which
|
||||
includes CPU, I/O and RAM usage.
|
||||
|
||||
### Viewing PubSub
|
||||
|
||||
Suppose you have the following nodes and items.
|
||||
Suppose you have the following PubSub nodes and items.
|
||||
|
||||
|PubSub |Node |Item |
|
||||
|Jabber ID |Node |Item |
|
||||
|--- |--- |--- |
|
||||
|blog.jmp.chat |urn:xmpp:microblog:0|launch-2023 |
|
||||
|edhelas%40movim.eu|urn:xmpp:microblog:0|working-on-launching-the-movim-network-qPBzwc |
|
||||
|goffi%40goffi.org |urn:xmpp:microblog:0|libervia-v0-8-la-cecilia-BdQ4 |
|
||||
|blog.jmp.chat |urn:xmpp:microblog:0 |launch-2023 |
|
||||
|edhelas%40movim.eu |urn:xmpp:microblog:0 |working-on-launching-the-movim-network-qPBzwc |
|
||||
|goffi%40goffi.org |urn:xmpp:microblog:0 |libervia-v0-8-la-cecilia-BdQ4 |
|
||||
|news.movim.eu |Phoronix | |
|
||||
|pubsub.movim.eu |berlin-xmpp-meetup |7363a41d-1146-40b3-ac0f-8ee2559591a3 |
|
||||
|pubsub.movim.eu |berlin-xmpp-meetup |let-s-talk-about-the-xsf-and-possibly-other-things-6A8eV4|
|
||||
|pubsub.movim.eu |berlin-xmpp-meetup |let-s-talk-about-the-xsf-and-possibly-other-things-6A8eV4 |
|
||||
|pubsub.movim.eu |jesus-christ-son-of-god|the-passion-of-christ-redemption-and-salvation-for-all-who-believe-moSqXO|
|
||||
|pubsub.woodpeckersnest.space|PlanetJabber | |
|
||||
|pubsub.woodpeckersnest.space|xmpp-it | |
|
||||
|pubsub%40sure.im |news | |
|
||||
|
||||
#### To view pubsub nodes
|
||||
|
||||
```
|
||||
http://127.0.0.1:8000/opml?pubsub=news.movim.eu
|
||||
http://127.0.0.1:8000/opml?pubsub=pubsub.woodpeckersnest.space
|
||||
```
|
||||
|
||||
#### To view node items
|
||||
|
||||
|
@ -84,6 +138,10 @@ Suppose you have the following nodes and items.
|
|||
http://127.0.0.1:8000/atom?pubsub=edhelas%40movim.eu&node=urn%3Axmpp%3Amicroblog%3A0
|
||||
http://127.0.0.1:8000/atom?pubsub=news.movim.eu&node=Phoronix
|
||||
http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=berlin-xmpp-meetup
|
||||
http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=jesus-christ-son-of-god
|
||||
http://127.0.0.1:8000/atom?pubsub=pubsub.woodpeckersnest.space&node=PlanetJabber
|
||||
http://127.0.0.1:8000/atom?pubsub=pubsub.woodpeckersnest.space&node=xmpp-it
|
||||
http://127.0.0.1:8000/atom?pubsub=pubsub%40sure.im&node=news
|
||||
```
|
||||
|
||||
#### To view a node item
|
||||
|
@ -94,8 +152,15 @@ http://127.0.0.1:8000/atom?pubsub=edhelas%40movim.eu&node=urn%3Axmpp%3Amicroblog
|
|||
http://127.0.0.1:8000/atom?pubsub=goffi%40goffi.org&node=urn%3Axmpp%3Amicroblog%3A0&item=libervia-v0-8-la-cecilia-BdQ4
|
||||
http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=berlin-xmpp-meetup&item=7363a41d-1146-40b3-ac0f-8ee2559591a3
|
||||
http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=berlin-xmpp-meetup&item=let-s-talk-about-the-xsf-and-possibly-other-things-6A8eV4
|
||||
http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=jesus-christ-son-of-god&item=the-passion-of-christ-redemption-and-salvation-for-all-who-believe-moSqXO
|
||||
```
|
||||
|
||||
## Supported XEPs
|
||||
|
||||
- [XEP-0060: Publish-Subscribe](https://xmpp.org/extensions/xep-0060.html)
|
||||
- [XEP-0277: Microblogging over XMPP](https://xmpp.org/extensions/xep-0277.html)
|
||||
- [XEP-0472: Pubsub Social Feed](https://xmpp.org/extensions/xep-0472.html)
|
||||
|
||||
## Author
|
||||
|
||||
Schimon Jehudah Zackary
|
||||
|
@ -110,10 +175,30 @@ Python code is licensed under the license AGPL-3.0 only.
|
|||
|
||||
## Acknowledgement
|
||||
|
||||
Special thanks to "d3x" and "cchianel" from IRC channel #python on irc.libera.chat
|
||||
Thank you to Mr. Peter Saint-Andre ([stpeter](https://stpeter.im/journal/731.html))
|
||||
for manifesting [Atom Over XMPP](https://www.ietf.org/archive/id/draft-saintandre-atompub-notify-07.html).
|
||||
|
||||
Thank you to Mr. Wojtek and [Tigase](https://tigase.org/) for publicly exposing
|
||||
an implementation of PubSub as Syndication at [Sure.IM](https://sure.im/) as
|
||||
[feeds.tigase.im](http://feeds.tigase.im/atom?server=pubsub@sure.im&node=news).
|
||||
|
||||
Thank you to to Mr. Timothée Jaussoin ([edhelas](https://edhelas.movim.eu/)) who
|
||||
consistently and earnestly showcases the potential of PubSub as a publication
|
||||
platform with project [Movim](https://movim.eu/).
|
||||
|
||||
A special thank you to the gentlemen "d3x" and "cchianel" from IRC channel
|
||||
#python on irc.libera.chat for initial references concerning code, servers and
|
||||
FastAPI.
|
||||
|
||||
And an important thank you to Mr. Simone Canaletti
|
||||
([roughnecks](https://blog.woodpeckersnest.space)) for testing and deploying
|
||||
Rivista into production.
|
||||
|
||||
## Similar Projects
|
||||
|
||||
* [AtomEntry](https://github.com/tigase/sureim/blob/master/site/src/main/java/tigase/sure/web/site/client/pubsub/AtomEntry.java) and [PubSubPublishViewImpl](https://github.com/tigase/sureim/blob/master/site/src/main/java/tigase/sure/web/site/client/pubsub/PubSubPublishViewImpl.java) - Convert XMPP Pubsub Nodes to Atom Syndication Format and convey them over HTTP.
|
||||
* [AtomEntry](https://github.com/tigase/sureim/blob/master/site/src/main/java/tigase/sure/web/site/client/pubsub/AtomEntry.java)
|
||||
and [PubSubPublishViewImpl](https://github.com/tigase/sureim/blob/master/site/src/main/java/tigase/sure/web/site/client/pubsub/PubSubPublishViewImpl.java) -
|
||||
Convert XMPP Pubsub Nodes to Atom Syndication Format and convey them over HTTP.
|
||||
|
||||
* [AtomToPubsub](https://github.com/edhelas/atomtopubsub) - A little client that parses Atom + RSS feeds and send them on XMPP Pubsub Nodes.
|
||||
* [AtomToPubsub](https://github.com/edhelas/atomtopubsub) - A little client that
|
||||
parses Atom + RSS feeds and send them on XMPP Pubsub Nodes.
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[account]
|
||||
xmpp = ""
|
||||
pass = ""
|
|
@ -1,117 +0,0 @@
|
|||
* {
|
||||
color: #eee;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
h1#title, h2#subtitle, #actions, #references {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
#actions, #references {
|
||||
border-bottom: 1px solid #eee;
|
||||
line-height: 150%;
|
||||
padding: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#actions > *, #references * {
|
||||
letter-spacing: 5px;
|
||||
margin: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#header, #menu {
|
||||
line-height: 120%;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#menu > h3 {
|
||||
padding-left: 2%;
|
||||
}
|
||||
|
||||
#menu > ul > li {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#note {
|
||||
line-height: 30px;
|
||||
margin: auto;
|
||||
max-width: 70%;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#references {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
|
||||
#articles > ul > li > div > p.content {
|
||||
font-size: 120%;
|
||||
line-height: 30px;
|
||||
margin: auto;
|
||||
padding-left: 2%;
|
||||
padding-right: 10%;
|
||||
/* text-align: justify; */
|
||||
}
|
||||
|
||||
#articles > ul > li > div.entry {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
#articles > ul > li > div.entry h1 {
|
||||
font-size: 2vw;
|
||||
}
|
||||
|
||||
#articles > ul > li > div.entry h2 {
|
||||
font-size: 1.5vw;
|
||||
}
|
||||
|
||||
#selection-page {
|
||||
background: #000;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
line-height: 200%;
|
||||
overflow: overlay;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#selection-page h3 {
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
}
|
||||
|
||||
#selection-page span {
|
||||
display: inline-grid;
|
||||
margin: 2%
|
||||
}
|
||||
|
||||
#selection-page img {
|
||||
height: 128px;
|
||||
margin-bottom: 20%;
|
||||
margin-top: 20%;
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
#selection-page #selection {
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
|
||||
#selection-page #return {
|
||||
font-style: italic;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#selection-link, #selection-page #return {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#import datetime
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import feedgenerator
|
||||
from slixmpp import ClientXMPP
|
||||
import xml.etree.ElementTree as ET
|
||||
#import importlib.resources
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except:
|
||||
import tomli as tomllib
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class XmppInstance(ClientXMPP):
|
||||
def __init__(self, jid, password):
|
||||
super().__init__(jid, password)
|
||||
self.register_plugin('xep_0060')
|
||||
self.connect()
|
||||
# self.process(forever=False)
|
||||
|
||||
xmpp = None
|
||||
|
||||
# Mount static graphic, script and stylesheet directories
|
||||
app.mount("/css", StaticFiles(directory="css"), name="css")
|
||||
app.mount("/graphic", StaticFiles(directory="graphic"), name="graphic")
|
||||
app.mount("/script", StaticFiles(directory="script"), name="script")
|
||||
app.mount("/xsl", StaticFiles(directory="xsl"), name="xsl")
|
||||
|
||||
@app.get('/favicon.ico', include_in_schema=False)
|
||||
async def favicon():
|
||||
return FileResponse('favicon.ico')
|
||||
|
||||
@app.get('/atom')
|
||||
async def view_pubsub(request: Request):
|
||||
global xmpp
|
||||
if not xmpp:
|
||||
with open('configuration.toml', mode="rb") as configuration:
|
||||
credentials = tomllib.load(configuration)['account']
|
||||
xmpp = XmppInstance(credentials['xmpp'], credentials['pass'])
|
||||
# xmpp.connect()
|
||||
|
||||
pubsub = request.query_params.get('pubsub', '')
|
||||
node = request.query_params.get('node', '')
|
||||
item_id = request.query_params.get('item', '')
|
||||
if pubsub and node and item_id:
|
||||
iq = await xmpp.plugin['xep_0060'].get_item(pubsub, node, item_id)
|
||||
link = 'xmpp:{pubsub}?;node={node};item={item}'.format(
|
||||
pubsub=pubsub, node=node, item=item_id)
|
||||
xml_atom = pubsub_to_atom(iq, link)
|
||||
result = append_stylesheet(xml_atom)
|
||||
elif pubsub and node:
|
||||
iq = await xmpp.plugin['xep_0060'].get_items(pubsub, node)
|
||||
link = 'xmpp:{pubsub}?;node={node}'.format(pubsub=pubsub, node=node)
|
||||
xml_atom = pubsub_to_atom(iq, link)
|
||||
result = append_stylesheet(xml_atom)
|
||||
elif pubsub:
|
||||
iq = await xmpp.plugin['xep_0060'].get_nodes(pubsub)
|
||||
link = 'xmpp:{pubsub}'.format(pubsub=pubsub)
|
||||
result = pubsub_to_opml(iq)
|
||||
elif node:
|
||||
result = 'PubSub parameter is missing.'
|
||||
else:
|
||||
result = ('Mandatory parameter PubSub and '
|
||||
'optional parameter Node are missing.')
|
||||
return Response(content=result, media_type="application/xml")
|
||||
|
||||
def pubsub_to_atom(iq, link):
|
||||
"""Convert XEP-0060: Publish-Subscribe to RFC 4287: The Atom Syndication Format."""
|
||||
feed = feedgenerator.Atom1Feed(
|
||||
description = ('This is a syndication feed generated with PubSub to '
|
||||
'Atom, which conveys XEP-0060: Publish-Subscribe nodes '
|
||||
'to standard RFC 4287: The Atom Syndication Format.'),
|
||||
language = iq['pubsub']['items']['lang'],
|
||||
link = link,
|
||||
subtitle = 'XMPP PubSub Syndication Feed',
|
||||
title = iq['pubsub']['items']['node'])
|
||||
# See also iq['pubsub']['items']['substanzas']
|
||||
entries = iq['pubsub']['items']
|
||||
for entry in entries:
|
||||
item = entry['payload']
|
||||
namespace = '{http://www.w3.org/2005/Atom}'
|
||||
title = item.find(namespace + 'title')
|
||||
title = None if title == None else title.text
|
||||
feed_url = 'gemini://schimon.i2p/'
|
||||
updated = item.find(namespace + 'updated')
|
||||
updated = None if updated == None else updated.text
|
||||
# if updated: updated = datetime.datetime.fromisoformat(updated)
|
||||
content = item.find(namespace + 'content')
|
||||
content = 'No content' if content == None else content.text
|
||||
link = item.find(namespace + 'link')
|
||||
link = '' if link == None else link.attrib['href']
|
||||
author = item.find(namespace + 'author')
|
||||
if author and author.attrib: print(author.attrib)
|
||||
author = 'None' if author == None else author.text
|
||||
# create entry
|
||||
feed.add_item(
|
||||
description = content,
|
||||
# enclosure = feedgenerator.Enclosure(enclosure, enclosure_size, enclosure_type) if args.entry_enclosure else None,
|
||||
link = link,
|
||||
# pubdate = updated,
|
||||
title = title,
|
||||
unique_id = link)
|
||||
xml_atom = feed.writeString('utf-8')
|
||||
xml_atom_extended = append_element(
|
||||
xml_atom,
|
||||
'generator',
|
||||
'XPTA: XMPP PubSub To Atom')
|
||||
return xml_atom_extended
|
||||
|
||||
"""Patch function to append elements which are not provided by feedgenerator"""
|
||||
def append_element(xml_data, element, text):
|
||||
root = ET.fromstring(xml_data)
|
||||
|
||||
# Create the generator element
|
||||
generator_element = ET.Element(element)
|
||||
generator_element.text = text
|
||||
|
||||
# Append the generator element to the root
|
||||
root.append(generator_element)
|
||||
|
||||
# Return the modified XML as a string
|
||||
return ET.tostring(root, encoding='unicode')
|
||||
|
||||
"""Patch function to append XSLT reference to XML"""
|
||||
"""Why is not this a built-in function of ElementTree or LXML"""
|
||||
def append_stylesheet(xml_data):
|
||||
# Register namespace in order to avoide ns0:
|
||||
ET.register_namespace("", "http://www.w3.org/2005/Atom")
|
||||
# Load XML from string
|
||||
tree = ET.fromstring(xml_data)
|
||||
# The following direction removes the XML declaration
|
||||
xml_data_no_declaration = ET.tostring(tree, encoding='unicode')
|
||||
# Add XML declaration and stylesheet
|
||||
xml_data_declaration = ('<?xml version="1.0" encoding="utf-8"?>'
|
||||
'<?xml-stylesheet type="text/xsl" href="xsl/stylesheet.xsl"?>' +
|
||||
xml_data_no_declaration)
|
||||
return xml_data_declaration
|
63
pyproject.toml
Normal file
|
@ -0,0 +1,63 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=61.2"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "Rivista"
|
||||
version = "1.0"
|
||||
description = "A private journal publication and a content management system for XMPP"
|
||||
authors = [{name = "Schimon Zachary", email = "sch@fedora.email"}]
|
||||
license = {text = "AGPL-3.0"}
|
||||
classifiers = [
|
||||
"Framework :: slixmpp",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: AGPL-3.0 License",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Topic :: Internet :: Extensible Messaging and Presence Protocol (XMPP)",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary",
|
||||
"Topic :: Internet :: XMPP",
|
||||
"Topic :: Office/Business :: News/Diary",
|
||||
]
|
||||
keywords = [
|
||||
"atom",
|
||||
"blog",
|
||||
"cms",
|
||||
"gemini",
|
||||
"jabber",
|
||||
"journal",
|
||||
"news",
|
||||
"ssg",
|
||||
"syndication",
|
||||
"xml",
|
||||
"xmpp",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"beautifulsoup4",
|
||||
"fastapi",
|
||||
"gmcapsule",
|
||||
"lxml",
|
||||
"markdown",
|
||||
# "markdown-text-clean",
|
||||
# "md2gemini",
|
||||
"python-dateutil",
|
||||
"slixmpp",
|
||||
"tomli", # Python 3.10
|
||||
"uvicorn",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://schapps.woodpeckersnest.eu/rivista/"
|
||||
Repository = "https://git.xmpp-it.net/sch/Rivista"
|
||||
Issues = "https://git.xmpp-it.net/sch/Rivista/issues"
|
||||
|
||||
[project.scripts]
|
||||
rivista = "rivista.__main__:main"
|
||||
|
||||
[tool.setuptools]
|
||||
platforms = ["any"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"*" = ["*.css", "*.js", "*.ico", "*.png", "*.svg", "*.toml", "*.xsl"]
|
3
rivista/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from rivista.version import __version__, __version_info__
|
||||
|
||||
print('Rivista XJP', __version__)
|
114
rivista/__main__.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#import importlib.resources
|
||||
|
||||
import os
|
||||
from rivista.config import Cache, Data, Settings
|
||||
from rivista.utilities import Toml
|
||||
from rivista.http.instance import HttpInstance
|
||||
import shutil
|
||||
import uvicorn
|
||||
|
||||
def main():
|
||||
http_instance = HttpInstance()
|
||||
return http_instance.app
|
||||
|
||||
if __name__ == 'rivista.__main__':
|
||||
|
||||
directory = os.path.dirname(__file__)
|
||||
|
||||
# Copy data files
|
||||
# FIXME File settins.toml is copied too.
|
||||
directory_data = Data.get_directory()
|
||||
if not os.path.exists(directory_data):
|
||||
directory_assets = os.path.join(directory, 'assets')
|
||||
directory_assets_new = shutil.copytree(directory_assets, directory_data)
|
||||
print(f'Data directory {directory_assets_new} has been created and populated.')
|
||||
|
||||
# Copy settings files
|
||||
directory_settings = Settings.get_directory()
|
||||
if not os.path.exists(directory_settings):
|
||||
directory_configs = os.path.join(directory, 'configs')
|
||||
directory_settings_new = shutil.copytree(directory_configs, directory_settings)
|
||||
print(f'Settings directory {directory_settings_new} has been created and populated.')
|
||||
|
||||
# Create cache directories
|
||||
directory_cache = Cache.get_directory()
|
||||
if not os.path.exists(directory_cache):
|
||||
print(f'Creating a cache directory at {directory_cache}.')
|
||||
os.mkdir(directory_cache)
|
||||
# TODO Add more cache directories (see JabberCard).
|
||||
# TODO Remove json (indices is the new name)
|
||||
for subdirectory in ('json', 'indices', 'pubsub'):
|
||||
subdirectory_cache = os.path.join(directory_cache, subdirectory)
|
||||
if not os.path.exists(subdirectory_cache):
|
||||
print(f'Creating a cache subdirectory at {subdirectory_cache}.')
|
||||
os.mkdir(subdirectory_cache)
|
||||
|
||||
# Configure settings file
|
||||
file_settings = os.path.join(directory_settings, 'settings.toml')
|
||||
if not os.path.exists(file_settings):
|
||||
directory_configs = os.path.join(directory, 'configs')
|
||||
file_settings_empty = os.path.join(directory_configs, 'settings.toml')
|
||||
shutil.copyfile(file_settings_empty, file_settings)
|
||||
data_settings = Toml.open_file_toml(file_settings)
|
||||
|
||||
# Configure account
|
||||
data_settings_account = data_settings['account']
|
||||
|
||||
settings_account = {
|
||||
'xmpp': 'Set a Jabber ID',
|
||||
'pass': 'Input Password'
|
||||
}
|
||||
|
||||
for key in settings_account:
|
||||
data_settings_account_value = data_settings_account[key]
|
||||
if not data_settings_account_value:
|
||||
settings_account_message = settings_account[key]
|
||||
while not data_settings_account_value:
|
||||
data_settings_account_value = input(f'{settings_account_message}: ')
|
||||
data_settings_account[key] = data_settings_account_value
|
||||
|
||||
# Configure default PubSub Service and Node Name
|
||||
data_settings_default = data_settings['default']
|
||||
|
||||
settings_default_default = {
|
||||
'pubsub': 'pubsub.woodpeckersnest.space',
|
||||
'nodeid': 'PlanetJabber'
|
||||
}
|
||||
|
||||
for key in settings_default_default:
|
||||
data_settings_default_value = data_settings_default[key]
|
||||
if not data_settings_default_value:
|
||||
settings_default_default_value = settings_default_default[key]
|
||||
data_settings_default_value = input(f'Set brand {key} (default: "{settings_default_default_value}"): ')
|
||||
if not data_settings_default_value: data_settings_default_value = settings_default_default_value
|
||||
data_settings_default[key] = data_settings_default_value
|
||||
|
||||
# Configure Settings
|
||||
data_settings_settings = data_settings['settings']
|
||||
|
||||
data_settings_settings_service = data_settings_settings['service']
|
||||
if data_settings_settings_service is '':
|
||||
while data_settings_settings_service not in ('n', 'N', 'no', 'No', 'y', 'Y', 'yes', 'Yes'):
|
||||
data_settings_settings_service = input(f'Run as service? [Y/n] ')
|
||||
if data_settings_settings_service in ('y', 'Y', 'yes', 'Yes'):
|
||||
data_settings_settings['service'] = 1
|
||||
else:
|
||||
data_settings_settings['service'] = 0
|
||||
|
||||
data_settings_settings_include = data_settings_settings['include']
|
||||
if data_settings_settings_include is '':
|
||||
data_settings_settings_include = input(f'Limit service to included domains? (comma separated. remain empty for no limit) ')
|
||||
data_settings_settings['include'] = data_settings_settings_include if data_settings_settings_include else 0
|
||||
|
||||
data_settings_settings_operator = data_settings_settings['operator']
|
||||
if data_settings_settings_operator is '':
|
||||
data_settings_settings['operator'] = input(f'Set a Jabber ID of an operator to contact with: ')
|
||||
|
||||
Toml.save_to_toml(file_settings, data_settings)
|
||||
|
||||
# Start JabberCard
|
||||
app = main()
|
||||
uvicorn.run(app, host='localhost', port=8000, reload=False)
|
303
rivista/assets/css/stylesheet.css
Normal file
|
@ -0,0 +1,303 @@
|
|||
/*
|
||||
|
||||
TODO
|
||||
|
||||
pubsub: news.movim.eu
|
||||
node: fake-news
|
||||
item: fdef84f5-e3e1-41ea-9b68-af4bb9130f77
|
||||
title: #14October2023EpochEclipse
|
||||
date: Fri, 15 Jan 2021 20:24:46
|
||||
|
||||
*/
|
||||
|
||||
* {
|
||||
color: #eee;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
*/
|
||||
|
||||
body {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
max-width: 100%; }
|
||||
|
||||
img, svg, video {
|
||||
display: block;
|
||||
max-height: 500px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
h1#title, h2#subtitle, #actions, #references {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
h5.related > a {
|
||||
margin-right: 5px;
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
|
||||
}
|
||||
|
||||
h3.title > a {
|
||||
display: block;
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
#actions, #references {
|
||||
border-bottom: 1px solid #eee;
|
||||
line-height: 150%;
|
||||
padding: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#actions > *, #references * {
|
||||
letter-spacing: 5px;
|
||||
margin: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#header, #menu {
|
||||
line-height: 120%;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#menu > h3 {
|
||||
padding-left: 2%;
|
||||
}
|
||||
|
||||
#menu > ol > li {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#references {
|
||||
margin-top: 5em;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
#articles {
|
||||
min-height: 80vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#articles #journal {
|
||||
margin-left: 2%;
|
||||
margin-right: 2%;
|
||||
margin-top: 2%;
|
||||
min-width: 350px;
|
||||
padding-bottom: 0.67em;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
#journal {
|
||||
margin: auto;
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
#articles > ul {
|
||||
margin: auto;
|
||||
margin-left: 2%;
|
||||
margin-top: 2%;
|
||||
}
|
||||
|
||||
#articles #journal ol,
|
||||
#articles #journal ul {
|
||||
/* height: 500px; */
|
||||
line-height: 160%;
|
||||
overflow: auto;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#articles div.content {
|
||||
font-size: 120%;
|
||||
line-height: 200%;
|
||||
margin: auto;
|
||||
padding-left: 2%;
|
||||
padding-right: 10%;
|
||||
/* text-align: justify; */
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#articles div.entry {
|
||||
padding-bottom: 0.67em;
|
||||
}
|
||||
|
||||
#articles div.entry h1 {
|
||||
font-size: 115%; /* 2vw */
|
||||
}
|
||||
|
||||
#articles div.entry h2 {
|
||||
font-size: 1.5vw;
|
||||
}
|
||||
|
||||
#selection-page {
|
||||
background: #000;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
line-height: 200%;
|
||||
overflow: overlay;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#selection-page p {
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
}
|
||||
|
||||
#selection-page span {
|
||||
display: inline-grid;
|
||||
margin: 2%
|
||||
}
|
||||
|
||||
#selection-page img {
|
||||
height: 128px;
|
||||
margin-bottom: 20%;
|
||||
margin-top: 20%;
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
#selection-page #selection {
|
||||
margin-bottom: 2%;
|
||||
}
|
||||
|
||||
#selection-page #return {
|
||||
/* font-style: italic; */
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#selection-link, #selection-page #return {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.content[type='text'] {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#articles div.entry span.tags {
|
||||
display: inline-flex;
|
||||
/* display: ruby; */
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#articles div.entry span.tags > div {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.enclosures {
|
||||
cursor: help;
|
||||
direction: ltr;
|
||||
margin: 5px auto 15px 1%;
|
||||
padding: 15px;
|
||||
padding: 1em;
|
||||
/* background: #222;
|
||||
border: 1px solid GrayText;
|
||||
border-radius: 4px;
|
||||
border-radius: .5em;
|
||||
color: #525c66;
|
||||
border-left: double;
|
||||
max-width: 40%; */
|
||||
}
|
||||
|
||||
.enclosure a {
|
||||
margin: 3px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.enclosure > span[icon='atom']:after,
|
||||
.enclosure > span[icon='html5']:after,
|
||||
.enclosure > span[icon='rss']:after {
|
||||
content: '📰';
|
||||
margin:3px;
|
||||
}
|
||||
|
||||
.enclosure > span.audio:after {
|
||||
content: ' (Audio file) ';
|
||||
}
|
||||
|
||||
.enclosure > span[icon='audio']:after{
|
||||
content: '🎼️';
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.enclosure > span:after {
|
||||
content: ' (Document file) ';
|
||||
}
|
||||
|
||||
.enclosure > span[icon]:after {
|
||||
content: '📄️';
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.enclosure > span.executable:after{
|
||||
content: ' (Executable file) ';
|
||||
}
|
||||
|
||||
.enclosure > span[icon='executable']:after {
|
||||
content: '📦️';
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.enclosure > span.image:after {
|
||||
content: ' (Image file) ';
|
||||
}
|
||||
|
||||
.enclosure > span[icon='image']:after {
|
||||
content: '🖼️';
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.enclosure > span.video:after {
|
||||
content: ' (Video file) ';
|
||||
}
|
||||
|
||||
.enclosure > span[icon='video']:after {
|
||||
content: '📽️';
|
||||
margin:3px;
|
||||
}
|
||||
|
||||
#note, #small {
|
||||
line-height: 30px;
|
||||
margin: auto;
|
||||
margin-top: 0.67em;
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
@media (max-width: 1550px) {
|
||||
#articles {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
#articles #journal {
|
||||
margin-right: unset;
|
||||
min-width: unset;
|
||||
width: unset;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
724
rivista/assets/graphic/leechcraft.svg
Normal file
|
@ -0,0 +1,724 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.0"
|
||||
width="96"
|
||||
height="96"
|
||||
id="svg2408"
|
||||
inkscape:version="0.48.1 r9760"
|
||||
sodipodi:docname="main.svg"
|
||||
inkscape:export-filename="/home/buckstabu/Dropbox/Public/lcicons/lcproto2.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1020"
|
||||
id="namedview68"
|
||||
showgrid="false"
|
||||
inkscape:zoom="6.6666666"
|
||||
inkscape:cx="53.537878"
|
||||
inkscape:cy="36.716849"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-4"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2408" />
|
||||
<defs
|
||||
id="defs2410">
|
||||
<linearGradient
|
||||
id="linearGradient4017">
|
||||
<stop
|
||||
style="stop-color:#ff6600;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4019" />
|
||||
<stop
|
||||
style="stop-color:#ff1e00;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop4021" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient4145">
|
||||
<stop
|
||||
style="stop-color:#ff5d00;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4147" />
|
||||
<stop
|
||||
style="stop-color:#00b1ff;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop4149" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3224">
|
||||
<stop
|
||||
id="stop3226"
|
||||
style="stop-color:#42394b;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3228"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3465">
|
||||
<stop
|
||||
id="stop3467"
|
||||
style="stop-color:#919191;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3469"
|
||||
style="stop-color:#fdfdfd;stop-opacity:1"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3187">
|
||||
<stop
|
||||
id="stop3189"
|
||||
style="stop-color:#b4b4b4;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3191"
|
||||
style="stop-color:#e6e6e6;stop-opacity:1"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
x1="45.447727"
|
||||
y1="92.539597"
|
||||
x2="45.447727"
|
||||
y2="7.0165396"
|
||||
id="ButtonShadow"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="scale(1.0058652,0.994169)">
|
||||
<stop
|
||||
id="stop3750"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3752"
|
||||
style="stop-color:#000000;stop-opacity:0.58823532"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3737">
|
||||
<stop
|
||||
id="stop3739"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3741"
|
||||
style="stop-color:#ffffff;stop-opacity:0"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<filter
|
||||
color-interpolation-filters="sRGB"
|
||||
id="filter3174">
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur3176"
|
||||
stdDeviation="1.71" />
|
||||
</filter>
|
||||
<linearGradient
|
||||
x1="36.357143"
|
||||
y1="6"
|
||||
x2="36.357143"
|
||||
y2="63.893143"
|
||||
id="linearGradient3188"
|
||||
xlink:href="#linearGradient3737"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
x1="48"
|
||||
y1="90"
|
||||
x2="48"
|
||||
y2="5.9877172"
|
||||
id="linearGradient3427"
|
||||
xlink:href="#linearGradient3187"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
cx="47.098362"
|
||||
cy="78.286942"
|
||||
r="38.957481"
|
||||
fx="47.098362"
|
||||
fy="78.286942"
|
||||
id="radialGradient3205"
|
||||
xlink:href="#linearGradient3224"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.3830053,-1.0350102e-8,0,1.3123934,-18.038924,-24.218731)" />
|
||||
<linearGradient
|
||||
x1="43.17857"
|
||||
y1="13"
|
||||
x2="43.17857"
|
||||
y2="83.294571"
|
||||
id="linearGradient3279"
|
||||
xlink:href="#linearGradient3465"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0294118,0,0,1.0294118,-1.4117647,-1.411765)" />
|
||||
<linearGradient
|
||||
id="linearGradient3737-7">
|
||||
<stop
|
||||
id="stop3739-2"
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3741-7"
|
||||
style="stop-color:#ffffff;stop-opacity:0"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
cx="48"
|
||||
cy="90.171875"
|
||||
r="42"
|
||||
fx="48"
|
||||
fy="90.171875"
|
||||
id="radialGradient2874"
|
||||
xlink:href="#linearGradient3737-7"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.1573129,0,0,0.99590774,-7.551021,0.1971319)" />
|
||||
<linearGradient
|
||||
id="linearGradient3737-4">
|
||||
<stop
|
||||
id="stop3739-8"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3741-71"
|
||||
style="stop-color:#ffffff;stop-opacity:0"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<clipPath
|
||||
id="clipPath3613">
|
||||
<rect
|
||||
width="84"
|
||||
height="84"
|
||||
rx="6"
|
||||
ry="6"
|
||||
x="6"
|
||||
y="6"
|
||||
id="rect3615"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" />
|
||||
</clipPath>
|
||||
<filter
|
||||
x="-0.192"
|
||||
y="-0.192"
|
||||
width="1.3839999"
|
||||
height="1.3839999"
|
||||
color-interpolation-filters="sRGB"
|
||||
id="filter3794">
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur3796"
|
||||
stdDeviation="5.28" />
|
||||
</filter>
|
||||
<linearGradient
|
||||
x1="48"
|
||||
y1="20.220806"
|
||||
x2="48"
|
||||
y2="138.66119"
|
||||
id="linearGradient2908"
|
||||
xlink:href="#linearGradient3737-4"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
x1="45.447727"
|
||||
y1="92.539597"
|
||||
x2="45.447727"
|
||||
y2="7.0165396"
|
||||
id="ButtonShadow-0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0058652,0,0,0.994169,100,0)">
|
||||
<stop
|
||||
id="stop3750-8"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3752-5"
|
||||
style="stop-color:#000000;stop-opacity:0.58823532"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
x1="32.251034"
|
||||
y1="6.1317081"
|
||||
x2="32.251034"
|
||||
y2="90.238609"
|
||||
id="linearGradient3780"
|
||||
xlink:href="#ButtonShadow-0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" />
|
||||
<linearGradient
|
||||
x1="32.251034"
|
||||
y1="6.1317081"
|
||||
x2="32.251034"
|
||||
y2="90.238609"
|
||||
id="linearGradient3772"
|
||||
xlink:href="#ButtonShadow-0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" />
|
||||
<linearGradient
|
||||
x1="32.251034"
|
||||
y1="6.1317081"
|
||||
x2="32.251034"
|
||||
y2="90.238609"
|
||||
id="linearGradient3725"
|
||||
xlink:href="#ButtonShadow-0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" />
|
||||
<linearGradient
|
||||
x1="32.251034"
|
||||
y1="6.1317081"
|
||||
x2="32.251034"
|
||||
y2="90.238609"
|
||||
id="linearGradient3721"
|
||||
xlink:href="#ButtonShadow-0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(0,-97)" />
|
||||
<linearGradient
|
||||
x1="32.251034"
|
||||
y1="6.1317081"
|
||||
x2="32.251034"
|
||||
y2="90.238609"
|
||||
id="linearGradient3224-3"
|
||||
xlink:href="#ButtonShadow-0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3737-4"
|
||||
id="linearGradient3053"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="48"
|
||||
y1="20.220806"
|
||||
x2="48"
|
||||
y2="138.66119" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3224"
|
||||
id="radialGradient3071"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.3830053,-1.0350102e-8,0,1.3123934,-17.140614,-23.729195)"
|
||||
cx="47.098362"
|
||||
cy="78.286942"
|
||||
fx="47.098362"
|
||||
fy="78.286942"
|
||||
r="38.957481" />
|
||||
<linearGradient
|
||||
id="linearGradient3304">
|
||||
<stop
|
||||
id="stop3306"
|
||||
style="stop-color:#ff8313;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3308"
|
||||
style="stop-color:#ffe400;stop-opacity:1"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3304"
|
||||
id="linearGradient3023"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.2146893,0,0,1.2146893,-90.000003,-246.69493)"
|
||||
x1="143.75"
|
||||
y1="334.5"
|
||||
x2="143.75"
|
||||
y2="276" />
|
||||
<linearGradient
|
||||
id="linearGradient3045">
|
||||
<stop
|
||||
id="stop3047"
|
||||
style="stop-color:#ff8313;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3049"
|
||||
style="stop-color:#ffe400;stop-opacity:1"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3737"
|
||||
id="linearGradient3894"
|
||||
x1="49.010517"
|
||||
y1="48.650372"
|
||||
x2="73.444344"
|
||||
y2="48.650372"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(2.1,0)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3737"
|
||||
id="linearGradient3896"
|
||||
x1="23.037859"
|
||||
y1="48.650372"
|
||||
x2="47.471485"
|
||||
y2="48.650372"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(2.1,0)" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#ButtonShadow-0"
|
||||
id="radialGradient3908"
|
||||
cx="61.227432"
|
||||
cy="48.650372"
|
||||
fx="61.227432"
|
||||
fy="48.650372"
|
||||
r="12.216913"
|
||||
gradientTransform="translate(2.1,7.9644297e-6)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#ButtonShadow-0"
|
||||
id="radialGradient3910"
|
||||
cx="35.254673"
|
||||
cy="48.650372"
|
||||
fx="35.254673"
|
||||
fy="48.650372"
|
||||
r="12.216814"
|
||||
gradientTransform="translate(2.1,-5.9733707e-6)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3224"
|
||||
id="radialGradient3955"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.4908884,-1.1072317e-8,0,1.4039702,-22.221992,-28.734351)"
|
||||
cx="47.098362"
|
||||
cy="78.286942"
|
||||
fx="47.098362"
|
||||
fy="78.286942"
|
||||
r="38.957481" />
|
||||
<radialGradient
|
||||
cx="47.098362"
|
||||
cy="78.286942"
|
||||
r="38.957481"
|
||||
fx="47.098362"
|
||||
fy="78.286942"
|
||||
id="radialGradient3205-1"
|
||||
xlink:href="#linearGradient3224-6"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.3830053,-1.0350102e-8,0,1.3123934,-18.038924,-24.218731)" />
|
||||
<linearGradient
|
||||
id="linearGradient3224-6">
|
||||
<stop
|
||||
id="stop3226-2"
|
||||
style="stop-color:#42394b;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3228-1"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
x1="48"
|
||||
y1="90"
|
||||
x2="48"
|
||||
y2="5.9877172"
|
||||
id="linearGradient3427-0"
|
||||
xlink:href="#linearGradient3187-5"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient3187-5">
|
||||
<stop
|
||||
id="stop3189-3"
|
||||
style="stop-color:#b4b4b4;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3191-2"
|
||||
style="stop-color:#e6e6e6;stop-opacity:1"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
x1="36.357143"
|
||||
y1="6"
|
||||
x2="36.357143"
|
||||
y2="63.893143"
|
||||
id="linearGradient3188-1"
|
||||
xlink:href="#linearGradient3737-9"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient3737-9">
|
||||
<stop
|
||||
id="stop3739-7"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3741-6"
|
||||
style="stop-color:#ffffff;stop-opacity:0"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
x1="43.17857"
|
||||
y1="13"
|
||||
x2="43.17857"
|
||||
y2="83.294571"
|
||||
id="linearGradient3279-8"
|
||||
xlink:href="#linearGradient3465-2"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0294118,0,0,1.0294118,-1.4117647,-1.411765)" />
|
||||
<linearGradient
|
||||
id="linearGradient3465-2">
|
||||
<stop
|
||||
id="stop3467-0"
|
||||
style="stop-color:#919191;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3469-6"
|
||||
style="stop-color:#fdfdfd;stop-opacity:1"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
cx="48"
|
||||
cy="90.171875"
|
||||
r="42"
|
||||
fx="48"
|
||||
fy="90.171875"
|
||||
id="radialGradient2874-4"
|
||||
xlink:href="#linearGradient3737-7-3"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.1573129,0,0,0.99590774,-7.551021,0.1971319)" />
|
||||
<linearGradient
|
||||
id="linearGradient3737-7-3">
|
||||
<stop
|
||||
id="stop3739-2-3"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3741-7-6"
|
||||
style="stop-color:#ffffff;stop-opacity:0"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
x1="48"
|
||||
y1="20.220806"
|
||||
x2="48"
|
||||
y2="138.66119"
|
||||
id="linearGradient2908-9"
|
||||
xlink:href="#linearGradient3737-4-0"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient3737-4-0">
|
||||
<stop
|
||||
id="stop3739-8-4"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3741-71-8"
|
||||
style="stop-color:#ffffff;stop-opacity:0"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<clipPath
|
||||
id="clipPath3613-5">
|
||||
<rect
|
||||
width="84"
|
||||
height="84"
|
||||
rx="6"
|
||||
ry="6"
|
||||
x="6"
|
||||
y="6"
|
||||
id="rect3615-1"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" />
|
||||
</clipPath>
|
||||
<filter
|
||||
x="-0.192"
|
||||
y="-0.192"
|
||||
width="1.3839999"
|
||||
height="1.3839999"
|
||||
color-interpolation-filters="sRGB"
|
||||
id="filter3794-2">
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur3796-9"
|
||||
stdDeviation="5.28" />
|
||||
</filter>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3187-5"
|
||||
id="linearGradient4094"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="48"
|
||||
y1="90"
|
||||
x2="48"
|
||||
y2="5.9877172" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3737-9"
|
||||
id="linearGradient4096"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="36.357143"
|
||||
y1="6"
|
||||
x2="36.357143"
|
||||
y2="63.893143" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3465-2"
|
||||
id="linearGradient4098"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0294118,0,0,1.0294118,-1.4117647,-1.411765)"
|
||||
x1="43.17857"
|
||||
y1="13"
|
||||
x2="43.17857"
|
||||
y2="83.294571" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3737-7-3"
|
||||
id="radialGradient4100"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.1573129,0,0,0.99590774,-7.551021,0.1971319)"
|
||||
cx="48"
|
||||
cy="90.171875"
|
||||
fx="48"
|
||||
fy="90.171875"
|
||||
r="42" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3737-4-0"
|
||||
id="linearGradient4102"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="48"
|
||||
y1="20.220806"
|
||||
x2="48"
|
||||
y2="138.66119" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3224-6"
|
||||
id="radialGradient4104"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.3830053,-1.0350102e-8,0,1.3123934,-18.038924,-24.218731)"
|
||||
cx="47.098362"
|
||||
cy="78.286942"
|
||||
fx="47.098362"
|
||||
fy="78.286942"
|
||||
r="38.957481" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3224-6"
|
||||
id="radialGradient4113"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.3830053,-1.0350102e-8,0,1.3123934,-17.140614,-23.729195)"
|
||||
cx="47.098362"
|
||||
cy="78.286942"
|
||||
fx="47.098362"
|
||||
fy="78.286942"
|
||||
r="38.957481" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4017"
|
||||
id="linearGradient4023"
|
||||
x1="48.732876"
|
||||
y1="37.021988"
|
||||
x2="48.609169"
|
||||
y2="60.526169"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata2413">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path
|
||||
style="opacity:0.6;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||
id="rect3977-9"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 17.89831,14.489536 c -1.662,0 -3,1.338 -3,3 l 0,62 c 0,1.662 1.338,3 3,3 l 62,0 c 1.662,0 3,-1.338 3,-3 l 0,-62 c 0,-1.662 -1.338,-3 -3,-3 l -62,0 z m 0,0.96875 62,0 c 1.140081,0 2.03125,0.891169 2.03125,2.03125 l 0,62 c 0,1.140081 -0.891169,2.03125 -2.03125,2.03125 l -62,0 c -1.140081,0 -2.03125,-0.891169 -2.03125,-2.03125 l 0,-62 c 0,-1.140081 0.891169,-2.03125 2.03125,-2.03125 z" />
|
||||
<g
|
||||
id="layer2"
|
||||
style="display:none">
|
||||
<rect
|
||||
width="86"
|
||||
height="85"
|
||||
rx="6"
|
||||
ry="6"
|
||||
x="5"
|
||||
y="7"
|
||||
id="rect3745"
|
||||
style="opacity:0.9;fill:url(#ButtonShadow);fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3174)" />
|
||||
</g>
|
||||
<rect
|
||||
style="fill:url(#radialGradient4113);fill-opacity:1;stroke:none"
|
||||
id="rect3461-7"
|
||||
y="8.743969"
|
||||
x="9.0391903"
|
||||
ry="6.1732869"
|
||||
rx="5.8527675"
|
||||
height="78.516495"
|
||||
width="77.914963" />
|
||||
<path
|
||||
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;opacity:0.15;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Abandoned Bitplane;-inkscape-font-specification:Abandoned Bitplane"
|
||||
id="rect3222-3-8"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 19.89831,13.489536 c -3.289406,0 -6,2.710594 -6,6 l 0,58 c 0,3.289406 2.710594,6 6,6 l 58,0 c 3.289406,0 6,-2.710594 6,-6 l 0,-58 c 0,-3.289406 -2.710594,-6 -6,-6 l -58,0 z m 0,4 58,0 c 1.142594,0 2,0.857406 2,2 l 0,58 c 0,1.142594 -0.857406,2 -2,2 l -58,0 c -1.142594,0 -2,-0.857406 -2,-2 l 0,-58 c 0,-1.142594 0.857406,-2 2,-2 z" />
|
||||
<path
|
||||
style="opacity:0.3;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||
id="rect3981-0"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 17.89831,14.489536 c -1.662,0 -3,1.338 -3,3 l 0,62 c 0,1.662 1.338,3 3,3 l 62,0 c 1.662,0 3,-1.338 3,-3 l 0,-62 c 0,-1.662 -1.338,-3 -3,-3 l -62,0 z m 0,1.9375 62,0 c 0.618163,0 1.0625,0.444337 1.0625,1.0625 l 0,62 c 0,0.618163 -0.444337,1.0625 -1.0625,1.0625 l -62,0 c -0.618163,0 -1.0625,-0.444337 -1.0625,-1.0625 l 0,-62 c 0,-0.618163 0.444337,-1.0625 1.0625,-1.0625 z" />
|
||||
<g
|
||||
id="g3912"
|
||||
style="fill:url(#linearGradient4023);fill-opacity:1.0;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
transform="matrix(1.2125503,0,0,1.2125503,-13.04106,-10.991024)">
|
||||
<path
|
||||
d="m 26.099813,37.395511 0,22.509723 22.50972,0 0,-7.118459 -15.198872,0 0,-15.391264 -7.310848,0 z"
|
||||
id="rect3294"
|
||||
style="fill:url(#linearGradient4023);fill-opacity:1.0;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 52.072568,37.395511 0,22.509723 22.509727,0 0,-7.310849 -15.583656,0 0,-7.888025 15.583656,0 0,-7.310849 -22.509727,0 z"
|
||||
id="rect3298"
|
||||
style="fill:url(#linearGradient4023);fill-opacity:1.0;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<path
|
||||
style="opacity:0.07999998;fill:#d9c6ed;fill-opacity:1;stroke:none"
|
||||
id="rect3256-3"
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 14.89831,8.7395363 c -3.242433,0 -5.8437503,2.7674997 -5.8437503,6.1874997 l 0,49.0625 C 27.878614,50.130638 54.866224,40.195879 86.96081,42.208286 l 0,-27.28125 c 0,-3.420001 -2.63257,-6.1874997 -5.875,-6.1874997 l -66.1875,0 z" />
|
||||
<g
|
||||
id="layer1"
|
||||
style="display:inline">
|
||||
<path
|
||||
d="M 12,6 C 8.676,6 6,8.676 6,12 l 0,72 c 0,3.324 2.676,6 6,6 l 72,0 c 3.324,0 6,-2.676 6,-6 L 90,12 C 90,8.676 87.324,6 84,6 L 12,6 z m 5,7 62,0 c 2.216001,0 4,1.784 4,4 l 0,62 c 10e-7,2.216001 -1.784,4 -4,4 l -62,0 c -2.215999,10e-7 -4,-1.784 -4,-4 l 0,-62 c 0,-2.215999 1.784,-4 4,-4 z"
|
||||
inkscape:connector-curvature="0"
|
||||
id="rect2419"
|
||||
style="fill:url(#linearGradient4094);fill-opacity:1;fill-rule:nonzero;stroke:none" />
|
||||
<path
|
||||
d="M 12,6 C 8.676,6 6,8.676 6,12 l 0,2 0,68 0,2 c 0,0.334721 0.04135,0.6507 0.09375,0.96875 0.0487,0.295596 0.09704,0.596915 0.1875,0.875 0.00988,0.03038 0.020892,0.0636 0.03125,0.09375 0.098865,0.287771 0.2348802,0.547452 0.375,0.8125 0.1445918,0.273507 0.3156161,0.535615 0.5,0.78125 0.1843839,0.245635 0.3737765,0.473472 0.59375,0.6875 0.439947,0.428056 0.94291,0.814526 1.5,1.09375 0.278545,0.139612 0.5734731,0.246947 0.875,0.34375 -0.2562018,-0.100222 -0.4867109,-0.236272 -0.71875,-0.375 -0.00741,-0.0044 -0.023866,0.0045 -0.03125,0 -0.031933,-0.0193 -0.062293,-0.04251 -0.09375,-0.0625 -0.120395,-0.0767 -0.2310226,-0.163513 -0.34375,-0.25 -0.1061728,-0.0808 -0.2132809,-0.161112 -0.3125,-0.25 C 8.4783201,88.557317 8.3087904,88.373362 8.15625,88.1875 8.0486711,88.057245 7.9378561,87.922215 7.84375,87.78125 7.818661,87.74287 7.805304,87.69538 7.78125,87.65625 7.716487,87.553218 7.6510225,87.451733 7.59375,87.34375 7.4927417,87.149044 7.3880752,86.928049 7.3125,86.71875 7.30454,86.69694 7.288911,86.6782 7.28125,86.65625 7.2494249,86.5643 7.2454455,86.469419 7.21875,86.375 7.1884177,86.268382 7.1483606,86.171969 7.125,86.0625 7.0521214,85.720988 7,85.364295 7,85 L 7,83 7,15 7,13 C 7,10.218152 9.2181517,8 12,8 l 2,0 68,0 2,0 c 2.781848,0 5,2.218152 5,5 l 0,2 0,68 0,2 c 0,0.364295 -0.05212,0.720988 -0.125,1.0625 -0.04415,0.206893 -0.08838,0.397658 -0.15625,0.59375 -0.0077,0.02195 -0.0233,0.04069 -0.03125,0.0625 -0.06274,0.173739 -0.138383,0.367449 -0.21875,0.53125 -0.04158,0.0828 -0.07904,0.169954 -0.125,0.25 -0.0546,0.09721 -0.126774,0.18835 -0.1875,0.28125 -0.09411,0.140965 -0.204921,0.275995 -0.3125,0.40625 -0.143174,0.17445 -0.303141,0.346998 -0.46875,0.5 -0.01117,0.0102 -0.01998,0.02115 -0.03125,0.03125 -0.138386,0.125556 -0.285091,0.234436 -0.4375,0.34375 -0.102571,0.07315 -0.204318,0.153364 -0.3125,0.21875 -0.0074,0.0045 -0.02384,-0.0044 -0.03125,0 -0.232039,0.138728 -0.462548,0.274778 -0.71875,0.375 0.301527,-0.0968 0.596455,-0.204138 0.875,-0.34375 0.55709,-0.279224 1.060053,-0.665694 1.5,-1.09375 0.219973,-0.214028 0.409366,-0.441865 0.59375,-0.6875 0.184384,-0.245635 0.355408,-0.507743 0.5,-0.78125 0.14012,-0.265048 0.276135,-0.524729 0.375,-0.8125 0.01041,-0.03078 0.02133,-0.06274 0.03125,-0.09375 0.09046,-0.278085 0.1388,-0.579404 0.1875,-0.875 C 89.95865,84.6507 90,84.334721 90,84 l 0,-2 0,-68 0,-2 C 90,8.676 87.324,6 84,6 L 12,6 z"
|
||||
inkscape:connector-curvature="0"
|
||||
id="rect3728"
|
||||
style="opacity:0.5;fill:url(#linearGradient4096);fill-opacity:1;fill-rule:nonzero;stroke:none" />
|
||||
<path
|
||||
d="m 17,12 c -2.752703,0 -5,2.247297 -5,5 l 0,62 c 0,2.752703 2.247298,5 5,5 l 62,0 c 2.752703,0 5.000001,-2.247297 5,-5 l 0,-62 c 0,-2.752703 -2.247297,-5 -5,-5 l -62,0 z m 0,2 62,0 c 1.679298,0 3,1.320703 3,3 l 0,62 c 10e-7,1.679297 -1.320703,3 -3,3 l -62,0 c -1.679296,0 -3,-1.320703 -3,-3 l 0,-62 c 0,-1.679297 1.320703,-3 3,-3 z"
|
||||
inkscape:connector-curvature="0"
|
||||
id="rect3425"
|
||||
style="fill:url(#linearGradient4098);fill-opacity:1;fill-rule:nonzero;stroke:none" />
|
||||
<path
|
||||
d="M 12,90 C 8.676,90 6,87.324 6,84 L 6,82 6,14 6,12 c 0,-0.334721 0.04135,-0.6507 0.09375,-0.96875 0.0487,-0.295596 0.09704,-0.596915 0.1875,-0.875 C 6.29113,10.12587 6.302142,10.09265 6.3125,10.0625 6.411365,9.774729 6.5473802,9.515048 6.6875,9.25 6.8320918,8.976493 7.0031161,8.714385 7.1875,8.46875 7.3718839,8.223115 7.5612765,7.995278 7.78125,7.78125 8.221197,7.353194 8.72416,6.966724 9.28125,6.6875 9.559795,6.547888 9.8547231,6.440553 10.15625,6.34375 9.9000482,6.443972 9.6695391,6.580022 9.4375,6.71875 c -0.00741,0.0044 -0.023866,-0.0045 -0.03125,0 -0.031933,0.0193 -0.062293,0.04251 -0.09375,0.0625 -0.120395,0.0767 -0.2310226,0.163513 -0.34375,0.25 -0.1061728,0.0808 -0.2132809,0.161112 -0.3125,0.25 C 8.4783201,7.442683 8.3087904,7.626638 8.15625,7.8125 8.0486711,7.942755 7.9378561,8.077785 7.84375,8.21875 7.818661,8.25713 7.805304,8.30462 7.78125,8.34375 7.716487,8.446782 7.6510225,8.548267 7.59375,8.65625 7.4927417,8.850956 7.3880752,9.071951 7.3125,9.28125 7.30454,9.30306 7.288911,9.3218 7.28125,9.34375 7.2494249,9.4357 7.2454455,9.530581 7.21875,9.625 7.1884177,9.731618 7.1483606,9.828031 7.125,9.9375 7.0521214,10.279012 7,10.635705 7,11 l 0,2 0,68 0,2 c 0,2.781848 2.2181517,5 5,5 l 2,0 68,0 2,0 c 2.781848,0 5,-2.218152 5,-5 l 0,-2 0,-68 0,-2 C 89,10.635705 88.94788,10.279012 88.875,9.9375 88.83085,9.730607 88.78662,9.539842 88.71875,9.34375 88.71105,9.3218 88.69545,9.30306 88.6875,9.28125 88.62476,9.107511 88.549117,8.913801 88.46875,8.75 88.42717,8.6672 88.38971,8.580046 88.34375,8.5 88.28915,8.40279 88.216976,8.31165 88.15625,8.21875 88.06214,8.077785 87.951329,7.942755 87.84375,7.8125 87.700576,7.63805 87.540609,7.465502 87.375,7.3125 87.36383,7.3023 87.35502,7.29135 87.34375,7.28125 87.205364,7.155694 87.058659,7.046814 86.90625,6.9375 86.803679,6.86435 86.701932,6.784136 86.59375,6.71875 c -0.0074,-0.0045 -0.02384,0.0044 -0.03125,0 -0.232039,-0.138728 -0.462548,-0.274778 -0.71875,-0.375 0.301527,0.0968 0.596455,0.204138 0.875,0.34375 0.55709,0.279224 1.060053,0.665694 1.5,1.09375 0.219973,0.214028 0.409366,0.441865 0.59375,0.6875 0.184384,0.245635 0.355408,0.507743 0.5,0.78125 0.14012,0.265048 0.276135,0.524729 0.375,0.8125 0.01041,0.03078 0.02133,0.06274 0.03125,0.09375 0.09046,0.278085 0.1388,0.579404 0.1875,0.875 C 89.95865,11.3493 90,11.665279 90,12 l 0,2 0,68 0,2 c 0,3.324 -2.676,6 -6,6 l -72,0 z"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3615"
|
||||
style="opacity:0.4;fill:url(#radialGradient4100);fill-opacity:1;fill-rule:nonzero;stroke:none" />
|
||||
<rect
|
||||
width="66"
|
||||
height="66"
|
||||
rx="12"
|
||||
ry="12"
|
||||
x="15"
|
||||
y="15"
|
||||
clip-path="url(#clipPath3613-5)"
|
||||
id="rect3171"
|
||||
style="opacity:0.1;fill:url(#linearGradient4102);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;filter:url(#filter3794-2)" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 8 KiB After Width: | Height: | Size: 8 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
8
rivista/assets/script/iso8601_to_utc.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Convert ISO8601 To UTC
|
||||
|
||||
window.onload = function(){
|
||||
for (element of document.querySelectorAll('#articles > ul > li > div > h4')) {
|
||||
timestamp = new Date(element.textContent);
|
||||
element.textContent = timestamp.toUTCString();
|
||||
}
|
||||
}
|
7
rivista/assets/script/parse_markdown.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Parse Markdown
|
||||
|
||||
window.onload = function(){
|
||||
for (element of document.querySelectorAll('#articles > ul > li > div > p')) {
|
||||
element.innerHTML = marked.parse(element.textContent);
|
||||
}
|
||||
}
|
265
rivista/assets/script/postprocess.js
Normal file
|
@ -0,0 +1,265 @@
|
|||
window.onload = async function(){
|
||||
let locationHref = new URL(location.href);
|
||||
let node = locationHref.searchParams.get('node')
|
||||
let pubsub = locationHref.searchParams.get('pubsub')
|
||||
|
||||
// Set button follow
|
||||
let follow = document.querySelector('#follow');
|
||||
if (follow) {
|
||||
//let feedUrl = location.href.replace(/^https?:/, 'feed:');
|
||||
let feedUrl = `feed://${location.host}/atom?pubsub=${pubsub}&node=${node}`;
|
||||
follow.href = feedUrl;
|
||||
follow.type = `application/atom+xml`;
|
||||
follow.addEventListener ('click', function() {
|
||||
window.open(feedUrl, "_self");
|
||||
});
|
||||
// Fix button subtome
|
||||
document.querySelector('#subtome').href='https://www.subtome.com/#/subscribe?feeds=' + feedUrl;
|
||||
}
|
||||
|
||||
// Convert ISO8601 To UTC
|
||||
for (let element of document.querySelectorAll(
|
||||
'#articles > ul > li > div > h4.published,' +
|
||||
'#articles > ul > li > div > h4.updated, ' +
|
||||
'#feed > #header > h2#subtitle.date')) {
|
||||
let timeStamp = new Date(element.textContent);
|
||||
element.textContent = timeStamp.toUTCString();
|
||||
}
|
||||
|
||||
// Parse Markdown
|
||||
for (let element of document.querySelectorAll('#articles div[type="text"]')) {
|
||||
element.innerHTML = marked.parse(element.textContent);
|
||||
}
|
||||
|
||||
// NOTE Report this issue to Movim. See node "deltachat" of pubsub "news.movim.eu".
|
||||
for (let element of document.querySelectorAll('#articles div[type="html"]')) {
|
||||
if (!element.children.length) {
|
||||
element.innerHTML = marked.parse(element.textContent);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
NOTE
|
||||
|
||||
The reason for the following code to parse HTML inside an software which
|
||||
already parses HTML, is that some people who influence the so called Gecko
|
||||
HTML Engine product (mostly people from the advertisement industry with whom
|
||||
I shamefully used to work with) have intentions to eliminate standards and
|
||||
technologies such as Syndication (Atom/RDF/RSS), XSLT and many other
|
||||
technologies that actually empower people and their privacy.
|
||||
|
||||
Recently, some changes were made to the XSLT parser of Gecko which result in
|
||||
noncompliance with the XSLT standard.
|
||||
|
||||
The XSLT feature that was removed is "disable-output-escaping" which upon the
|
||||
value "yes" the XSLT engine should transform and treat a subject string into
|
||||
HTML, yet the Gecko HTML Engine ignores this XSLT direction.
|
||||
|
||||
This change was probably made in order to:
|
||||
* Frustrate new XSLT developers; or
|
||||
* Influence new XSLT developers to think that XSLT has a limited set of
|
||||
features and believing of some sort of inability to parse HTML from a
|
||||
retrieved HTML string; and
|
||||
* Consequently cause developers to abstain from using the XSLT technology.
|
||||
|
||||
Do not use HTML browsers or use Ladybird, Pale Moon or browsers that are
|
||||
powered by KHTML (WebKit) instead of anti-privacy software such as Chromium
|
||||
and Gecko.
|
||||
|
||||
*/
|
||||
|
||||
// Parse HTML
|
||||
//if (navigator.userAgent.includes(') Gecko/')) {
|
||||
// for (let element of document.querySelectorAll('#articles div[type="html"]')) {
|
||||
// element.innerHTML = element.textContent;
|
||||
// }
|
||||
//}
|
||||
for (let element of document.querySelectorAll('#articles div[type="html"]')) {
|
||||
if (!element.children.length) {
|
||||
element.innerHTML = element.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
// Build a journal list
|
||||
if (locationHref.pathname.startsWith('/atom') && pubsub && node && !node.includes('/')) {
|
||||
itemsList = await openJson(pubsub, node)
|
||||
if (itemsList && locationHref.searchParams.get('item')) {
|
||||
let elementDiv = document.createElement('div');
|
||||
elementDiv.id = 'journal';
|
||||
let elementH3 = document.createElement('h3');
|
||||
elementH3.textContent = 'Journal';
|
||||
elementDiv.appendChild(elementH3);
|
||||
let elementH4 = document.createElement('h4');
|
||||
elementH4.textContent = node;
|
||||
elementDiv.appendChild(elementH4);
|
||||
let elementUl = document.createElement('ol');
|
||||
elementDiv.appendChild(elementUl);
|
||||
for (let item of itemsList.reverse()) {
|
||||
let elementLi = document.createElement('li');
|
||||
let elementA = document.createElement('a');
|
||||
elementA.textContent = item.title;
|
||||
elementA.href = item.link;
|
||||
elementLi.appendChild(elementA);
|
||||
elementUl.appendChild(elementLi);
|
||||
console.log(elementLi.length)
|
||||
if (elementUl.children.length > 9) {break};
|
||||
}
|
||||
let elementB = document.createElement('b');
|
||||
elementB.textContent = 'Actions';
|
||||
elementDiv.appendChild(elementB);
|
||||
let elementUl2 = document.createElement('ul');
|
||||
elementDiv.appendChild(elementUl2);
|
||||
links = [
|
||||
{'text' : 'Subscribe from an XMPP client.',
|
||||
'href' : `xmpp:${pubsub}?pubsub;action=subscribe;node=${node}`,
|
||||
'type' : 'x-scheme-handler/xmpp'},
|
||||
{'text' : 'Subscribe with a News Reader.',
|
||||
'href' : `feed://${location.host}/atom?pubsub=${pubsub}&node=${node}`,
|
||||
'type' : 'application/atom+xml'},
|
||||
{'text' : 'Browse the journal.',
|
||||
'href' : `atom?pubsub=${pubsub}&node=${node}`,
|
||||
'type' : 'application/atom+xml'},
|
||||
{'text' : 'Browse the portal.',
|
||||
'href' : `opml?pubsub=${pubsub}`,
|
||||
'type' : 'text/x-opml'}
|
||||
]
|
||||
for (let link of links) {
|
||||
let elementLi = document.createElement('li');
|
||||
let elementA = document.createElement('a');
|
||||
elementA.textContent = link.text;
|
||||
elementA.href = link.href;
|
||||
elementA.type = link.type;
|
||||
elementLi.appendChild(elementA);
|
||||
elementUl2.appendChild(elementLi);
|
||||
}
|
||||
elementDiv.appendChild(elementUl2);
|
||||
// document.querySelector('#feed').appendChild(elementDiv); // This would result in a combination of Title, Article, and Journal
|
||||
document.querySelector('#articles').appendChild(elementDiv);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert URI xmpp: to URI http: links.
|
||||
for (let xmppLink of document.querySelectorAll(
|
||||
'#articles h3 > a[href^="xmpp:"][id^="rivista-"],' +
|
||||
'#articles h5.related > a[class^="rivista-"],' +
|
||||
'#journal > ol > li > a[href^="xmpp:"]')) {
|
||||
xmppUri = new URL(xmppLink);
|
||||
let parameters = xmppUri.search.split(';');
|
||||
try {
|
||||
try {
|
||||
let node = parameters.find(parameter => parameter.startsWith('node=')).split('=')[1];
|
||||
let item = parameters.find(parameter => parameter.startsWith('item=')).split('=')[1];
|
||||
let pubsub = xmppUri.pathname;
|
||||
xmppLink.href = `atom?pubsub=${pubsub}&node=${node}&item=${item}`
|
||||
} catch {
|
||||
let node = parameters.find(parameter => parameter.startsWith('node=')).split('=')[1];
|
||||
let pubsub = xmppUri.pathname;
|
||||
xmppLink.href = `atom?pubsub=${pubsub}&node=${node}`
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Display a selection of suggested software.
|
||||
const selection = {
|
||||
'akregator' : {
|
||||
'name' : 'Akregator',
|
||||
'image' : 'akregator.svg',
|
||||
'url' : 'https://apps.kde.org/akregator/'
|
||||
},
|
||||
'leechcraft' : {
|
||||
'name' : 'LeechCraft',
|
||||
'image' : 'leechcraft.png',
|
||||
'url' : 'https://leechcraft.org/'
|
||||
},
|
||||
'liferea' : {
|
||||
'name' : 'Liferea',
|
||||
'image' : 'liferea.svg',
|
||||
'url' : 'https://lzone.de/liferea/'
|
||||
},
|
||||
'raven' : {
|
||||
'name' : 'Raven Reader',
|
||||
'image' : 'raven.svg',
|
||||
'url' : 'https://ravenreader.app/'
|
||||
},
|
||||
'rssguard' : {
|
||||
'name' : 'RSS Guard',
|
||||
'image' : 'rssguard.png',
|
||||
'url' : 'https://github.com/martinrotter/rssguard'
|
||||
},
|
||||
'rssowl' : {
|
||||
'name' : 'RSSOwl',
|
||||
'image' : 'rssowl.svg',
|
||||
'url' : 'http://www.rssowl.org/'
|
||||
},
|
||||
'tickr' : {
|
||||
'name' : 'TICKR',
|
||||
'image' : 'tickr.png',
|
||||
'url' : 'https://www.open-tickr.net/'
|
||||
}
|
||||
}
|
||||
let selectionLink = document.querySelector('#selection-link');
|
||||
selectionLink.addEventListener ('click', function() {
|
||||
let elementDiv = document.createElement('div');
|
||||
elementDiv.id = 'selection-page';
|
||||
let elementH1 = document.createElement('h1');
|
||||
elementH1.textContent = 'Select A News Reader';
|
||||
elementDiv.appendChild(elementH1);
|
||||
let elementH2 = document.createElement('h2');
|
||||
elementH2.textContent = 'Install A Feed Reader For Desktop And Mobile';
|
||||
elementDiv.appendChild(elementH2);
|
||||
const brands = Object.keys(selection);
|
||||
let elementDivSel = document.createElement('div');
|
||||
elementDivSel.id = 'selection';
|
||||
for (let i = 0; i < brands.length; i++) {
|
||||
let brand = brands[i];
|
||||
let elementSpan = document.createElement('span');
|
||||
let elementA = document.createElement('a');
|
||||
elementA.href = selection[brand].url;
|
||||
elementA.textContent = selection[brand].name;
|
||||
let elementImg = document.createElement('img');
|
||||
elementImg.src = 'graphic/' + selection[brand].image;
|
||||
elementSpan.appendChild(elementImg);
|
||||
elementSpan.appendChild(elementA);
|
||||
elementDivSel.appendChild(elementSpan);
|
||||
elementDiv.appendChild(elementDivSel);
|
||||
}
|
||||
let elementP1 = document.createElement('p');
|
||||
elementP1.textContent = '' +
|
||||
'This is a selection of desktop, mobile and HTML (sometimes referred to ' +
|
||||
'as "online") News Readers for you to choose from.';
|
||||
elementDiv.appendChild(elementP1);
|
||||
let elementP2 = document.createElement('p');
|
||||
elementP2.textContent = '' +
|
||||
'This selection includes: Podcast Managers, Torrent ' +
|
||||
'Clients, Chat Bots, HTML Browsers and Plugins which support ' +
|
||||
'syndication feeds.';
|
||||
elementDiv.appendChild(elementP2);
|
||||
let elementSpan = document.createElement('span');
|
||||
elementSpan.id = 'return';
|
||||
elementSpan.textContent = 'Continue Reading';
|
||||
elementSpan.addEventListener ('click', function() {
|
||||
document.querySelector('#selection-page').remove();
|
||||
});
|
||||
elementDiv.appendChild(elementSpan);
|
||||
document.body.appendChild(elementDiv);
|
||||
});
|
||||
}
|
||||
|
||||
async function openJson(pubsubJid, nodeId) {
|
||||
return fetch(`/data/${pubsubJid}/${nodeId}.json`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP Error: ' + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
return json;
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn(err);
|
||||
})
|
||||
}
|
24
rivista/assets/xsl/atom.xsl
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<!--
|
||||
Copyright (C) 2016 - 2024 Schimon Jehuda. Released under MIT license
|
||||
Feeds rendered using this XSLT stylesheet, or it's derivatives, must
|
||||
include https://schimon.i2p/ in attribute name='generator' of
|
||||
element <meta/> inside of html element </head>
|
||||
-->
|
||||
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output encoding = 'UTF-8'
|
||||
indent = 'yes'
|
||||
media-type = 'application/atom+xml'
|
||||
method = 'html'
|
||||
version = '4.01' />
|
||||
<!-- Atom Syndication Format 1.0 -->
|
||||
<xsl:include href='atom_as_xhtml.xsl'/>
|
||||
<!-- extract filename from given url string -->
|
||||
<xsl:include href='extract-filename.xsl'/>
|
||||
<!-- set page metadata -->
|
||||
<xsl:include href='metadata.xsl'/>
|
||||
<!-- transform filesize from given length string -->
|
||||
<xsl:include href='transform-filesize.xsl'/>
|
||||
</xsl:stylesheet>
|
531
rivista/assets/xsl/atom_as_xhtml.xsl
Normal file
|
@ -0,0 +1,531 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<!--
|
||||
Copyright (C) 2016 - 2024 Schimon Jehuda. Released under MIT license
|
||||
Feeds rendered using this XSLT stylesheet, or it's derivatives, must
|
||||
include https://schimon.i2p/ in attribute name='generator' of
|
||||
element <meta/> inside of html element </head>
|
||||
-->
|
||||
|
||||
<xsl:stylesheet version='1.0'
|
||||
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
|
||||
xmlns:xml='http://www.w3.org/XML/1998/namespace'
|
||||
xmlns:media='http://search.yahoo.com/mrss/'
|
||||
xmlns:georss='http://www.georss.org/georss'
|
||||
xmlns:geo='http://www.w3.org/2003/01/geo/wgs84_pos#'
|
||||
xmlns:atom10='http://www.w3.org/2005/Atom'
|
||||
xmlns:atom='http://www.w3.org/2005/Atom'>
|
||||
<xsl:template match='/atom:feed'>
|
||||
<!-- index right-to-left language codes -->
|
||||
<!-- TODO http://www.w3.org/TR/xpath/#function-lang -->
|
||||
<xsl:variable name='rtl'
|
||||
select='@xml:lang[
|
||||
contains(self::node(),"ar") or
|
||||
contains(self::node(),"fa") or
|
||||
contains(self::node(),"he") or
|
||||
contains(self::node(),"ji") or
|
||||
contains(self::node(),"ku") or
|
||||
contains(self::node(),"ur") or
|
||||
contains(self::node(),"yi")]'/>
|
||||
<html>
|
||||
<head>
|
||||
<xsl:call-template name='metadata'>
|
||||
<xsl:with-param name='name' select='"description"' />
|
||||
<xsl:with-param name='content' select='atom:subtitle' />
|
||||
</xsl:call-template>
|
||||
<xsl:call-template name='metadata'>
|
||||
<xsl:with-param name='name' select='"generator"' />
|
||||
<xsl:with-param name='content' select='Rivista' />
|
||||
</xsl:call-template>
|
||||
<xsl:call-template name='metadata'>
|
||||
<xsl:with-param name='name' select='"mimetype"' />
|
||||
<xsl:with-param name='content' select='"application/atom+xml"' />
|
||||
</xsl:call-template>
|
||||
<title>
|
||||
<xsl:choose>
|
||||
<xsl:when test='atom:title and not(atom:title="") and count(atom:entry) > 1'>
|
||||
<xsl:value-of select='atom:title'/>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:entry'>
|
||||
<xsl:value-of select='atom:entry/atom:title'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>Rivista</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</title>
|
||||
<!-- TODO media='print' -->
|
||||
<link rel='stylesheet' type='text/css' media='screen' href='/css/stylesheet.css'/>
|
||||
<link rel='icon' type='image/svg+xml' href='/graphic/xmpp.svg'/>
|
||||
<!-- whether language code is of direction right-to-left -->
|
||||
<xsl:if test='$rtl'>
|
||||
<link id='semitic' href='/css/stylesheet-rtl.css' rel='stylesheet' type='text/css' />
|
||||
</xsl:if>
|
||||
<script type='text/javascript' src='/script/marked.min.js'/>
|
||||
<script type='text/javascript' src='/script/postprocess.js'/>
|
||||
</head>
|
||||
<body>
|
||||
<div id='actions'>
|
||||
<a id='follow' title='Subscribe the latest updates and news.'
|
||||
onclick='window.open(location.href.replace(/^https?:/, "feed:"), "_self")'>
|
||||
<!-- xsl:attribute name="href">
|
||||
feed:<xsl:value-of select="atom:link[@rel='self']/@href" />
|
||||
</xsl:attribute -->
|
||||
Follow
|
||||
</a>
|
||||
<a id='subtome' title='Subscribe via SubToMe.'>
|
||||
<xsl:attribute name='href'>
|
||||
javascript:location.href='https://www.subtome.com/#/subscribe?feeds='+location.href;
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name='onclick'>
|
||||
(
|
||||
function(btn){
|
||||
var z=document.createElement('script');
|
||||
document.subtomeBtn=btn;
|
||||
z.src='https://www.subtome.com/load.js';document.body.appendChild(z);
|
||||
}
|
||||
)(this);
|
||||
return false;
|
||||
</xsl:attribute>
|
||||
SubToMe
|
||||
</a>
|
||||
<a href='https://xmpp.org/about/technology-overview/'
|
||||
title='Of the benefits of XMPP.'>
|
||||
XMPP
|
||||
</a>
|
||||
<a href='https://join.jabber.network/#syndication@conference.movim.eu?join'
|
||||
title='Syndictaion and PubSub.'>
|
||||
Groupchat
|
||||
</a>
|
||||
<a href='https://aboutfeeds.com/'
|
||||
title='Of the benefits of syndication feed.'
|
||||
id='aboutfeeds'>
|
||||
Help
|
||||
</a>
|
||||
</div>
|
||||
<div id='feed'>
|
||||
<div id='header'>
|
||||
<!-- feed title -->
|
||||
<h1 id='title'>
|
||||
<xsl:choose>
|
||||
<xsl:when test='atom:title and not(atom:title="") and count(atom:entry) > 1'>
|
||||
<xsl:value-of select='atom:title'/>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:entry'>
|
||||
<xsl:value-of select='atom:entry/atom:title'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
No title
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</h1>
|
||||
<!-- feed subtitle -->
|
||||
<h2 id='subtitle'>
|
||||
<xsl:choose>
|
||||
<xsl:when test='atom:title and not(atom:title="") and count(atom:entry) > 1'>
|
||||
<xsl:value-of select='atom:subtitle'/>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:entry'>
|
||||
<xsl:attribute name='class'><xsl:text>date</xsl:text></xsl:attribute>
|
||||
<xsl:value-of select='atom:entry/atom:updated'/>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:entry'>
|
||||
<xsl:attribute name='class'><xsl:text>date</xsl:text></xsl:attribute>
|
||||
<xsl:value-of select='atom:entry/atom:published'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
Rivista XMPP Journal Publisher
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</h2>
|
||||
</div>
|
||||
<xsl:if test='count(atom:entry) > 1'>
|
||||
<div id='menu'>
|
||||
<h3>Latest Posts</h3>
|
||||
<!-- xsl:for-each select='atom:entry[position() <21]' -->
|
||||
<ol>
|
||||
<xsl:for-each select='atom:entry[not(position() > 20)]'>
|
||||
<li>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:text>#rivista-</xsl:text>
|
||||
<xsl:value-of select='position()'/>
|
||||
</xsl:attribute>
|
||||
<xsl:choose>
|
||||
<xsl:when test='string-length(atom:title) > 0'>
|
||||
<xsl:value-of select='atom:title'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
*** No Title ***
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:element>
|
||||
</li>
|
||||
</xsl:for-each>
|
||||
</ol>
|
||||
</div>
|
||||
</xsl:if>
|
||||
<div id='articles'>
|
||||
<!-- feed entry -->
|
||||
<xsl:choose>
|
||||
<xsl:when test='atom:entry'>
|
||||
<ul>
|
||||
<xsl:for-each select='atom:entry[not(position() >20)]'>
|
||||
<li>
|
||||
<div class='entry'>
|
||||
<!-- entry title -->
|
||||
<h3 class='title'>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:choose>
|
||||
<xsl:when test='atom:link[@rel="self"]'>
|
||||
<xsl:value-of select='atom:link[@rel="self"]/@href'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<xsl:value-of select='atom:link/@href'/>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name='id'>
|
||||
<xsl:text>rivista-</xsl:text>
|
||||
<xsl:value-of select='position()'/>
|
||||
</xsl:attribute>
|
||||
<xsl:choose>
|
||||
<xsl:when test='string-length(atom:title) > 0'>
|
||||
<xsl:value-of select='atom:title'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
*** No Title ***
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:element>
|
||||
</h3>
|
||||
<!-- geographic location -->
|
||||
<xsl:choose>
|
||||
<xsl:when test='geo:lat and geo:long'>
|
||||
<xsl:variable name='lat' select='geo:lat'/>
|
||||
<xsl:variable name='lng' select='geo:long'/>
|
||||
<span class='geolocation'>
|
||||
<a href='geo:{$lat},{$lng}'>📍</a>
|
||||
</span>
|
||||
</xsl:when>
|
||||
<xsl:when test='geo:Point'>
|
||||
<xsl:variable name='lat' select='geo:Point/geo:lat'/>
|
||||
<xsl:variable name='lng' select='geo:Point/geo:long'/>
|
||||
<span class='geolocation'>
|
||||
<a href='geo:{$lat},{$lng}'>📍</a>
|
||||
</span>
|
||||
</xsl:when>
|
||||
<xsl:when test='georss:point'>
|
||||
<xsl:variable name='lat' select='substring-before(georss:point, " ")'/>
|
||||
<xsl:variable name='lng' select='substring-after(georss:point, " ")'/>
|
||||
<xsl:variable name='name' select='georss:featurename'/>
|
||||
<span class='geolocation'>
|
||||
<a href='geo:{$lat},{$lng}' title='{$name}'>📍</a>
|
||||
</span>
|
||||
</xsl:when>
|
||||
</xsl:choose>
|
||||
<!-- entry date -->
|
||||
<xsl:element name='h4'>
|
||||
<xsl:choose>
|
||||
<xsl:when test='atom:updated'>
|
||||
<xsl:attribute name='class'>
|
||||
<xsl:text>updated</xsl:text>
|
||||
</xsl:attribute>
|
||||
<xsl:value-of select='atom:updated'/>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:published'>
|
||||
<xsl:attribute name='class'>
|
||||
<xsl:text>published</xsl:text>
|
||||
</xsl:attribute>
|
||||
<xsl:value-of select='atom:published'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<h4 class='warning atom1 published'></h4>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:element>
|
||||
<!-- entry author -->
|
||||
<xsl:if test='atom:author'>
|
||||
<h4 class='author'>
|
||||
<xsl:text>By </xsl:text>
|
||||
<xsl:choose>
|
||||
<xsl:when test='atom:author/atom:email'>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:text>mailto:</xsl:text>
|
||||
<xsl:value-of select='atom:author/atom:email'/>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name='title'>
|
||||
<xsl:text>Send an Email to </xsl:text>
|
||||
<xsl:value-of select='atom:author/atom:email'/>
|
||||
</xsl:attribute>
|
||||
<xsl:value-of select='atom:author/atom:name'/>
|
||||
</xsl:element>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:author/atom:uri'>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:value-of select='atom:author/atom:uri'/>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name='title'>
|
||||
<xsl:value-of select='atom:author/atom:summary'/>
|
||||
</xsl:attribute>
|
||||
<xsl:value-of select='atom:author/atom:name'/>
|
||||
</xsl:element>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:author/atom:name'>
|
||||
<xsl:value-of select='atom:author/atom:name'/>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:uri'>
|
||||
<xsl:value-of select='atom:uri'/>
|
||||
</xsl:when>
|
||||
</xsl:choose>
|
||||
</h4>
|
||||
</xsl:if>
|
||||
<h5 class='related'>
|
||||
<xsl:if test='atom:link[@rel="alternate" and @type="x-scheme-handler/xmpp"]'>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:value-of select='atom:link[@rel="alternate" and @type="x-scheme-handler/xmpp"]/@href'/>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name='class'>
|
||||
<xsl:text>rivista-jabber</xsl:text>
|
||||
</xsl:attribute>
|
||||
💡️ Source
|
||||
</xsl:element>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:value-of select='atom:link[@rel="alternate" and @type="x-scheme-handler/xmpp"]/@href'/>
|
||||
</xsl:attribute>
|
||||
(XMPP)
|
||||
</xsl:element>
|
||||
</xsl:if>
|
||||
<xsl:if test='atom:link[@rel="contact"]'>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:value-of select='atom:link[@rel="contact"]/@href'/>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name='class'>
|
||||
<xsl:text>contact-uri</xsl:text>
|
||||
</xsl:attribute>
|
||||
🪪️ Contact
|
||||
</xsl:element>
|
||||
</xsl:if>
|
||||
<xsl:if test='atom:link[@rel="replies"]'>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:value-of select='atom:link[@rel="replies"]/@href'/>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name='class'>
|
||||
<xsl:text>rivista-replies</xsl:text>
|
||||
</xsl:attribute>
|
||||
💬 Discussion
|
||||
</xsl:element>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:value-of select='atom:link[@rel="replies"]/@href'/>
|
||||
</xsl:attribute>
|
||||
(XMPP)
|
||||
</xsl:element>
|
||||
</xsl:if>
|
||||
<xsl:if test='atom:link[@rel="alternate" and @type="text/html"]'>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:value-of select='atom:link[@rel="alternate" and @type="text/html"]/@href'/>
|
||||
</xsl:attribute>
|
||||
📜 HTML
|
||||
</xsl:element>
|
||||
</xsl:if>
|
||||
<xsl:if test='atom:link[@rel="related" and @type="text/html"]'>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:value-of select='atom:link[@rel="related" and @type="text/html"]/@href'/>
|
||||
</xsl:attribute>
|
||||
📜 HTML (Related)
|
||||
</xsl:element>
|
||||
</xsl:if>
|
||||
</h5>
|
||||
<!-- entry content -->
|
||||
<xsl:if test='atom:content or atom:summary'>
|
||||
<div class='content'>
|
||||
<xsl:choose>
|
||||
<xsl:when test='atom:summary[contains(@type,"text")]'>
|
||||
<xsl:attribute name='type'>
|
||||
<xsl:value-of select='atom:summary/@type'/>
|
||||
</xsl:attribute>
|
||||
<xsl:value-of select='atom:summary'/>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:summary[contains(@type,"html")]'>
|
||||
<xsl:attribute name='type'>
|
||||
<xsl:value-of select='atom:summary/@type'/>
|
||||
</xsl:attribute>
|
||||
<xsl:value-of select='atom:summary' disable-output-escaping='yes'/>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:summary[contains(@type,"base64")]'>
|
||||
<!-- TODO add xsl:template to handle inline media -->
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:content[contains(@type,"text")]'>
|
||||
<xsl:attribute name='type'>
|
||||
<xsl:value-of select='atom:content/@type'/>
|
||||
</xsl:attribute>
|
||||
<xsl:value-of select='atom:content'/>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:content[contains(@type,"html")]'>
|
||||
<xsl:attribute name='type'>
|
||||
<xsl:value-of select='atom:content/@type'/>
|
||||
</xsl:attribute>
|
||||
<xsl:value-of select='atom:content' disable-output-escaping='yes'/>
|
||||
</xsl:when>
|
||||
<xsl:when test='atom:content[contains(@type,"base64")]'>
|
||||
<!-- TODO add xsl:template to handle inline media -->
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<xsl:choose>
|
||||
<xsl:when test='atom:summary and not(atom:summary="")'>
|
||||
<xsl:value-of select='atom:summary' disable-output-escaping='yes'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<xsl:value-of select='atom:content' disable-output-escaping='yes'/>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</div>
|
||||
</xsl:if>
|
||||
<!-- entry tags -->
|
||||
<xsl:if test='atom:category/@term'>
|
||||
<h4>Tags</h4>
|
||||
<span class='tags'>
|
||||
<xsl:for-each select='atom:category'>
|
||||
<xsl:element name='div'>
|
||||
<xsl:value-of select='@term'/>
|
||||
</xsl:element>
|
||||
</xsl:for-each>
|
||||
</span>
|
||||
</xsl:if>
|
||||
<!-- entry enclosure -->
|
||||
<xsl:if test='atom:link[@rel="enclosure"]'>
|
||||
<h4>Media</h4>
|
||||
<span class='enclosures' title='Right-click and Save link as…'>
|
||||
<xsl:for-each select='atom:link[@rel="enclosure"]'>
|
||||
<div class='enclosure' title='Right-click and Save link as…'>
|
||||
<xsl:element name='span'>
|
||||
<xsl:attribute name='icon'>
|
||||
<xsl:value-of select='substring-before(@type,"/")'/>
|
||||
</xsl:attribute>
|
||||
</xsl:element>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:value-of select='@href'/>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name='download'/>
|
||||
<xsl:call-template name='extract-filename'>
|
||||
<xsl:with-param name='url' select='@href' />
|
||||
</xsl:call-template>
|
||||
</xsl:element>
|
||||
<xsl:element name='span'>
|
||||
<xsl:attribute name='class'>
|
||||
<xsl:value-of select='substring-before(@type,"/")'/>
|
||||
</xsl:attribute>
|
||||
</xsl:element>
|
||||
<xsl:if test='@length > 0'>
|
||||
<xsl:call-template name='transform-filesize'>
|
||||
<xsl:with-param name='length' select='@length' />
|
||||
</xsl:call-template>
|
||||
</xsl:if>
|
||||
<br/>
|
||||
</div>
|
||||
</xsl:for-each>
|
||||
</span>
|
||||
</xsl:if>
|
||||
</div>
|
||||
<!-- entry id -->
|
||||
<xsl:if test='not(atom:id)'>
|
||||
<div class='warning atom1 id'>No entry ID</div>
|
||||
</xsl:if>
|
||||
</li>
|
||||
</xsl:for-each>
|
||||
</ul>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<ul>
|
||||
<li>
|
||||
<div class='entry'>
|
||||
<h3 class='title'>
|
||||
<a href='javascript:alert("Please check that the mother PubSub node is populated with content.")'>
|
||||
No content
|
||||
</a>
|
||||
</h3>
|
||||
<h4>This entry is empty</h4>
|
||||
<div class='content'>Please check that the mother PubSub node is populated with content.</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</div>
|
||||
</div>
|
||||
<div id='references'>
|
||||
<a href='https://joinjabber.org/'
|
||||
title='An Inclusive Space On The Jabber Network.'>
|
||||
JoinJabber
|
||||
</a>
|
||||
<a href='https://libervia.org/'
|
||||
title='The Universal Communication Ecosystem.'>
|
||||
Libervia
|
||||
</a>
|
||||
<a href='https://join.movim.eu/'
|
||||
title='The Social Platform Shaped For Your Community.'>
|
||||
Movim
|
||||
</a>
|
||||
<a href='https://git.xmpp-it.net/sch/Rivista'
|
||||
title='A Journal Publisher And Browser For XMPP.'>
|
||||
Rivista
|
||||
</a>
|
||||
<a href='https://git.xmpp-it.net/sch/Blasta'
|
||||
title='A Social Bookmark Manager For XMPP.'>
|
||||
Blasta
|
||||
</a>
|
||||
<a href='https://github.com/SeveFP/Reeder'
|
||||
title='An XMPP-Based Feed Reader.'>
|
||||
Reeder
|
||||
</a>
|
||||
<a href='https://modernxmpp.org/'
|
||||
title='A Project To Improve The Quality Of User-To-User Messaging Applications That Use XMPP.'>
|
||||
Modern
|
||||
</a>
|
||||
<a href='https://xmpp.org/'
|
||||
title='The Universal Messaging Standard.'>
|
||||
XMPP
|
||||
</a>
|
||||
<a href='https://xmpp.org/extensions/xep-0060.html'
|
||||
title='XEP-0060: Publish-Subscribe.'>
|
||||
PubSub
|
||||
</a>
|
||||
</div>
|
||||
<!-- note -->
|
||||
<p id='note'>
|
||||
This an Atom document which can also be viewed with
|
||||
a Syndication Feed Reader (also referred to as News Reader
|
||||
or RSS Reader) which provides automated news updates and
|
||||
notifications on desktop and mobile.
|
||||
<span id="selection-link">Click here</span> for a
|
||||
selection of software and pick the ones that would fit
|
||||
to you best!
|
||||
</p>
|
||||
<p id='small'>
|
||||
<i>
|
||||
This ASF (Atom Syndication Format)
|
||||
document is conveyed as an XHTML document.
|
||||
This document was produced by an
|
||||
XSLT <a href="xsl/atom.xsl">stylesheet</a>.
|
||||
XSLT is a powerful technology which transforms XML
|
||||
documents into HTML, JSON, PDF, Text XHTML, and
|
||||
(modified) XML documents;
|
||||
<a href="https://www.w3.org/Style/XSL/">Learn more</a>
|
||||
about The Extensible Stylesheet Language Family (XSL).
|
||||
</i>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
20
rivista/assets/xsl/opml.xsl
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<!--
|
||||
Copyright (C) 2016 - 2024 Schimon Jehuda. Released under MIT license
|
||||
Feeds rendered using this XSLT stylesheet, or it's derivatives, must
|
||||
include https://schimon.i2p/ in attribute name='generator' of
|
||||
element <meta/> inside of html element </head>
|
||||
-->
|
||||
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output encoding = 'UTF-8'
|
||||
indent = 'yes'
|
||||
media-type = 'text/x-opml'
|
||||
method = 'html'
|
||||
version = '4.01' />
|
||||
<!-- Outline Processor Markup Language 1.0 -->
|
||||
<xsl:include href='opml_as_xhtml.xsl'/>
|
||||
<!-- set page metadata -->
|
||||
<xsl:include href='metadata.xsl'/>
|
||||
</xsl:stylesheet>
|
234
rivista/assets/xsl/opml_as_xhtml.xsl
Normal file
|
@ -0,0 +1,234 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<!--
|
||||
Copyright (C) 2016 - 2024 Schimon Jehuda. Released under MIT license
|
||||
Feeds rendered using this XSLT stylesheet, or it's derivatives, must
|
||||
include https://schimon.i2p/ in attribute name='generator' of
|
||||
element <meta/> inside of html element </head>
|
||||
-->
|
||||
|
||||
<xsl:stylesheet version='1.0'
|
||||
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
|
||||
xmlns:xml='http://www.w3.org/XML/1998/namespace'>
|
||||
<xsl:template match='/opml'>
|
||||
<!-- index right-to-left language codes -->
|
||||
<!-- TODO http://www.w3.org/TR/xpath/#function-lang -->
|
||||
<xsl:variable name='rtl'
|
||||
select='lang[
|
||||
contains(self::node(),"ar") or
|
||||
contains(self::node(),"fa") or
|
||||
contains(self::node(),"he") or
|
||||
contains(self::node(),"ji") or
|
||||
contains(self::node(),"ku") or
|
||||
contains(self::node(),"ur") or
|
||||
contains(self::node(),"yi")]'/>
|
||||
<html>
|
||||
<head>
|
||||
<xsl:call-template name='metadata'>
|
||||
<xsl:with-param name='name' select='"description"' />
|
||||
<xsl:with-param name='content' select='subtitle' />
|
||||
</xsl:call-template>
|
||||
<xsl:call-template name='metadata'>
|
||||
<xsl:with-param name='name' select='"generator"' />
|
||||
<xsl:with-param name='content' select='Rivista' />
|
||||
</xsl:call-template>
|
||||
<xsl:call-template name='metadata'>
|
||||
<xsl:with-param name='name' select='"mimetype"' />
|
||||
<xsl:with-param name='content' select='"text/x-opml"' />
|
||||
</xsl:call-template>
|
||||
<title>
|
||||
<xsl:choose>
|
||||
<xsl:when test='//head/title and not(//head/title="")'>
|
||||
<xsl:value-of select='//head/title'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>Rivista OPML</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</title>
|
||||
<!-- TODO media='print' -->
|
||||
<link rel='stylesheet' type='text/css' media='screen' href='/css/stylesheet.css'/>
|
||||
<link rel='icon' type='image/svg+xml' href='/graphic/xmpp.svg'/>
|
||||
<!-- whether language code is of direction right-to-left -->
|
||||
<xsl:if test='$rtl'>
|
||||
<link id='semitic' href='/css/stylesheet-rtl.css' rel='stylesheet' type='text/css' />
|
||||
</xsl:if>
|
||||
<script type='text/javascript' src='/script/postprocess.js'/>
|
||||
</head>
|
||||
<body>
|
||||
<div id='actions'>
|
||||
<a href='https://xmpp.org/about/technology-overview/'
|
||||
title='Of the benefits of XMPP.'>
|
||||
XMPP
|
||||
</a>
|
||||
<a href='https://join.jabber.network/#syndication@conference.movim.eu?join'
|
||||
title='Syndictaion and PubSub.'>
|
||||
Groupchat
|
||||
</a>
|
||||
<a href='https://aboutfeeds.com/'
|
||||
title='Of the benefits of syndication feed.'
|
||||
id='aboutfeeds'>
|
||||
Help
|
||||
</a>
|
||||
</div>
|
||||
<div id='feed'>
|
||||
<div id='header'>
|
||||
<!-- feed title -->
|
||||
<h1 id='title'>
|
||||
<xsl:choose>
|
||||
<xsl:when test='//head/title and not(//head/title="") and count(//outline) > 1'>
|
||||
<xsl:value-of select='//head/title'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
Rivista OPML Collection
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</h1>
|
||||
<!-- feed subtitle -->
|
||||
<h2 id='subtitle'>
|
||||
<xsl:choose>
|
||||
<xsl:when test='//head/description and not(//head/description="") and count(//outline) > 1'>
|
||||
<xsl:value-of select='//head/description'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
Outline Processor Markup Language
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</h2>
|
||||
</div>
|
||||
<xsl:if test='count(//outline) > 1'>
|
||||
<div id='menu'>
|
||||
<h3>Subscriptions</h3>
|
||||
<!-- xsl:for-each select='outline[position() <21]' -->
|
||||
<ol>
|
||||
<xsl:for-each select='//outline[not(position() > 20)]'>
|
||||
<li>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:text>#rivista-</xsl:text>
|
||||
<xsl:value-of select='position()'/>
|
||||
</xsl:attribute>
|
||||
<xsl:choose>
|
||||
<xsl:when test='string-length(@text) > 0'>
|
||||
<xsl:value-of select='@text'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
*** No Title ***
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:element>
|
||||
</li>
|
||||
</xsl:for-each>
|
||||
</ol>
|
||||
</div>
|
||||
</xsl:if>
|
||||
<div id='articles'>
|
||||
<!-- opml outline -->
|
||||
<xsl:choose>
|
||||
<xsl:when test='//outline'>
|
||||
<ul>
|
||||
<xsl:for-each select='//outline[not(position() > 20)]'>
|
||||
<li>
|
||||
<div class='entry'>
|
||||
<!-- outline title -->
|
||||
<h3 class='title'>
|
||||
<xsl:element name='a'>
|
||||
<xsl:attribute name='href'>
|
||||
<xsl:value-of select='@xmlUrl'/>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name='id'>
|
||||
<xsl:text>rivista-</xsl:text>
|
||||
<xsl:value-of select='position()'/>
|
||||
</xsl:attribute>
|
||||
<xsl:choose>
|
||||
<xsl:when test='string-length(@text) > 0'>
|
||||
<xsl:value-of select='@text'/>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
*** No Title ***
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:element>
|
||||
</h3>
|
||||
<!-- entry content -->
|
||||
<h4>
|
||||
<xsl:value-of select='@text'/>
|
||||
</h4>
|
||||
<p class='content'>
|
||||
<xsl:value-of select='@xmlUrl'/>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</xsl:for-each>
|
||||
</ul>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<div class='notice no-entry'></div>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</div>
|
||||
</div>
|
||||
<div id='references'>
|
||||
<a href='https://joinjabber.org/'
|
||||
title='An Inclusive Space On The Jabber Network.'>
|
||||
JoinJabber
|
||||
</a>
|
||||
<a href='https://libervia.org/'
|
||||
title='The Universal Communication Ecosystem.'>
|
||||
Libervia
|
||||
</a>
|
||||
<a href='https://join.movim.eu/'
|
||||
title='The Social Platform Shaped For Your Community.'>
|
||||
Movim
|
||||
</a>
|
||||
<a href='https://git.xmpp-it.net/sch/Rivista'
|
||||
title='A Journal Publisher And Browser For XMPP.'>
|
||||
Rivista
|
||||
</a>
|
||||
<a href='https://git.xmpp-it.net/sch/Blasta'
|
||||
title='A Social Bookmark Manager For XMPP.'>
|
||||
Blasta
|
||||
</a>
|
||||
<a href='https://github.com/SeveFP/Reeder'
|
||||
title='An XMPP-Based Feed Reader.'>
|
||||
Reeder
|
||||
</a>
|
||||
<a href='https://modernxmpp.org/'
|
||||
title='A Project To Improve The Quality Of User-To-User Messaging Applications That Use XMPP.'>
|
||||
Modern
|
||||
</a>
|
||||
<a href='https://xmpp.org/'
|
||||
title='The Universal Messaging Standard.'>
|
||||
XMPP
|
||||
</a>
|
||||
<a href='https://xmpp.org/extensions/xep-0060.html'
|
||||
title='XEP-0060: Publish-Subscribe.'>
|
||||
PubSub
|
||||
</a>
|
||||
</div>
|
||||
<!-- note -->
|
||||
<p id='note'>
|
||||
This is an OPML document which includes a collection of
|
||||
subscriptions, and it can be imported to
|
||||
a Syndication Feed Reader (also referred to as News Reader
|
||||
or RSS Reader) which provides automated news updates and
|
||||
notifications on desktop and mobile.
|
||||
<span id="selection-link">Click here</span> for a
|
||||
selection of software and pick the ones that would fit
|
||||
to you best!
|
||||
</p>
|
||||
<p id='small'>
|
||||
<i>
|
||||
This OPML (Outline Processor Markup Language)
|
||||
document is conveyed as an XHTML document.
|
||||
This document was produced by an
|
||||
XSLT <a href="xsl/opml.xsl">stylesheet</a>.
|
||||
XSLT is a powerful technology which transforms XML
|
||||
documents into HTML, JSON, PDF, Text XHTML, and
|
||||
(modified) XML documents;
|
||||
<a href="https://www.w3.org/Style/XSL/">Learn more</a>
|
||||
about The Extensible Stylesheet Language Family (XSL).
|
||||
</i>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
|
@ -14,7 +14,7 @@ element <meta/> inside of html element </head>
|
|||
indent = 'yes'
|
||||
omit-xml-decleration='no' />
|
||||
|
||||
<!-- Atom 1.0 Syndication Format -->
|
||||
<!-- Atom Syndication Format 1.0 -->
|
||||
<xsl:include href='atom_as_xhtml.xsl'/>
|
||||
|
||||
<!-- extract filename from given url string -->
|
|
@ -15,9 +15,13 @@ xmlns:georss='http://www.georss.org/georss'
|
|||
xmlns:geo='http://www.w3.org/2003/01/geo/wgs84_pos#'
|
||||
xmlns:atom10='http://www.w3.org/2005/Atom'
|
||||
xmlns:atom='http://www.w3.org/2005/Atom'>
|
||||
<!-- Atom 1.0 Syndication Format -->
|
||||
|
||||
<xsl:output
|
||||
media-type='application/atom+xml' />
|
||||
method = 'html'
|
||||
indent = 'yes'
|
||||
omit-xml-decleration='no' />
|
||||
|
||||
<!-- Atom 1.0 Syndication Format -->
|
||||
<xsl:template match='/atom:feed'>
|
||||
<!-- index right-to-left language codes -->
|
||||
<!-- TODO http://www.w3.org/TR/xpath/#function-lang -->
|
||||
|
@ -64,18 +68,18 @@ xmlns:atom='http://www.w3.org/2005/Atom'>
|
|||
</head>
|
||||
<body>
|
||||
<div id='actions'>
|
||||
<a title='Subscribe the latest updates and news.'
|
||||
onclick='window.open("feed:" + location.href, "_self")'>
|
||||
<a id='follow' title='Subscribe the latest updates and news.'
|
||||
onclick='window.open(location.href.replace(/^https?:/, "feed:"), "_self")'>
|
||||
<!-- xsl:attribute name="href">
|
||||
feed:<xsl:value-of select="atom:link[@rel='self']/@href" />
|
||||
</xsl:attribute -->
|
||||
Follow
|
||||
</a>
|
||||
<a title='Subscribe via SubToMe.'>
|
||||
<xsl:attribute name="href">
|
||||
https://www.subtome.com/#/subscribe?feeds=<xsl:value-of select="atom:link[@rel='self']/@href" />
|
||||
<a id='subtome' title='Subscribe via SubToMe.'>
|
||||
<xsl:attribute name='href'>
|
||||
javascript:location.href='https://www.subtome.com/#/subscribe?feeds='+location.href;
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name="onclick">
|
||||
<xsl:attribute name='onclick'>
|
||||
(
|
||||
function(btn){
|
||||
var z=document.createElement('script');
|
||||
|
@ -428,4 +432,70 @@ xmlns:atom='http://www.w3.org/2005/Atom'>
|
|||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
|
||||
|
||||
<!-- extract filename from given url string -->
|
||||
<!-- extract filename from given url string -->
|
||||
<xsl:template name='extract-filename'>
|
||||
<xsl:param name='url'/>
|
||||
<xsl:choose>
|
||||
<xsl:when test='contains($url,"/")'>
|
||||
<xsl:call-template name='extract-filename'>
|
||||
<xsl:with-param name='url' select='substring-after($url,"/")'/>
|
||||
</xsl:call-template>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<xsl:value-of select='$url'/>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:template>
|
||||
|
||||
<!-- set page metadata -->
|
||||
<!-- set page metadata -->
|
||||
<xsl:template name='metadata'>
|
||||
<xsl:param name='name'/>
|
||||
<xsl:param name='content'/>
|
||||
<xsl:if test='$content and not($content="")'>
|
||||
<xsl:element name='meta'>
|
||||
<xsl:attribute name='name'>
|
||||
<xsl:value-of select='$name'/>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name='content'>
|
||||
<xsl:value-of select='$content'/>
|
||||
</xsl:attribute>
|
||||
</xsl:element>
|
||||
</xsl:if>
|
||||
</xsl:template>
|
||||
|
||||
<!-- transform filesize from given length string -->
|
||||
<!-- transform filesize from given length string -->
|
||||
<xsl:template name='transform-filesize'>
|
||||
<xsl:param name='length'/>
|
||||
<!-- TODO consider xsl:decimal-format and xsl:number -->
|
||||
<xsl:choose>
|
||||
<!-- TODO consider removal of Byte -->
|
||||
<xsl:when test='$length < 2'>
|
||||
<xsl:value-of select='$length'/>
|
||||
Byte
|
||||
</xsl:when>
|
||||
<xsl:when test='floor($length div 1024) < 1'>
|
||||
<xsl:value-of select='$length'/>
|
||||
Bytes
|
||||
</xsl:when>
|
||||
<xsl:when test='floor($length div (1024 * 1024)) < 1'>
|
||||
<xsl:value-of select='floor($length div 1024)'/>.<xsl:value-of select='substring($length mod 1024,0,2)'/>
|
||||
KiB
|
||||
</xsl:when>
|
||||
<xsl:when test='floor($length div (1024 * 1024 * 1024)) < 1'>
|
||||
<xsl:value-of select='floor($length div (1024 * 1024))'/>.<xsl:value-of select='substring($length mod (1024 * 1024),0,2)'/>
|
||||
MiB
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<!-- P2P links -->
|
||||
<xsl:value-of select='floor($length div (1024 * 1024 * 1024))'/>.<xsl:value-of select='substring($length mod (1024 * 1024 * 1024),0,2)'/>
|
||||
GiB
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:template>
|
||||
|
||||
</xsl:stylesheet>
|
116
rivista/config.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Functions get_directory() were taken from project jarun/buku.
|
||||
By Arun Prakash Jana (jarun) and Dmitry Marakasov (AMDmi3).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
try:
|
||||
import tomllib
|
||||
except:
|
||||
import tomli as tomllib
|
||||
|
||||
class Settings:
|
||||
|
||||
def get_directory():
|
||||
"""
|
||||
Determine the directory path where setting files be stored.
|
||||
|
||||
* If $XDG_CONFIG_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to configuration directory.
|
||||
"""
|
||||
# config_home = xdg.BaseDirectory.xdg_config_home
|
||||
config_home = os.environ.get('XDG_CONFIG_HOME')
|
||||
if config_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
config_home = os.environ.get('APPDATA')
|
||||
if config_home is None:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
config_home = os.path.join(
|
||||
os.environ.get('HOME'), '.config'
|
||||
)
|
||||
return os.path.join(config_home, 'rivista')
|
||||
|
||||
def get_setting(filename, section):
|
||||
with open(filename, mode="rb") as settings:
|
||||
result = tomllib.load(settings)[section]
|
||||
return result
|
||||
|
||||
|
||||
class Data:
|
||||
|
||||
def get_directory():
|
||||
"""
|
||||
Determine the directory path where data files be stored.
|
||||
|
||||
* If $XDG_DATA_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to database file.
|
||||
"""
|
||||
# data_home = xdg.BaseDirectory.xdg_data_home
|
||||
data_home = os.environ.get('XDG_DATA_HOME')
|
||||
if data_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
data_home = os.environ.get('APPDATA')
|
||||
if data_home is None:
|
||||
return os.path.abspath('.rivista/data')
|
||||
else:
|
||||
return os.path.abspath('.rivista/data')
|
||||
else:
|
||||
data_home = os.path.join(
|
||||
os.environ.get('HOME'), '.local', 'share'
|
||||
)
|
||||
return os.path.join(data_home, 'rivista')
|
||||
|
||||
class Cache:
|
||||
|
||||
def get_directory():
|
||||
"""
|
||||
Determine the directory path where cache files be stored.
|
||||
|
||||
* If $XDG_CACHE_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to cache directory.
|
||||
"""
|
||||
# cache_home = xdg.BaseDirectory.xdg_cache_home
|
||||
cache_home = os.environ.get('XDG_CACHE_HOME')
|
||||
if cache_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
cache_home = os.environ.get('APPDATA')
|
||||
if cache_home is None:
|
||||
return os.path.abspath('.rivista/cache')
|
||||
else:
|
||||
return os.path.abspath('.rivista/cache')
|
||||
else:
|
||||
cache_home = os.path.join(
|
||||
os.environ.get('HOME'), '.cache'
|
||||
)
|
||||
return os.path.join(cache_home, 'rivista')
|
15
rivista/configs/settings.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
# An account to connect XMPP Journal Publisher to the XMPP network.
|
||||
[account]
|
||||
xmpp = "" # Jabber ID.
|
||||
pass = "" # Password.
|
||||
|
||||
# A default node, when no arguments are set.
|
||||
[default]
|
||||
pubsub = "" # Jabber ID.
|
||||
nodeid = "" # Node ID.
|
||||
|
||||
# Settings
|
||||
[settings]
|
||||
service = "" # Enable server as a service.
|
||||
include = "" # Limit service to a given domain.
|
||||
operator = "" # A Jabber ID to contact with, in case of an error.
|
4
rivista/gmi/index.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class GmiIndex:
|
52
rivista/gmi/markdown.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class GmiMarkdown:
|
||||
|
||||
def convert_to_gmi(markdown_text):
|
||||
lines = markdown_text.splitlines()
|
||||
footnotes = []
|
||||
footnote_counter = 1
|
||||
output_lines = []
|
||||
|
||||
for line in lines:
|
||||
# Process links in the format [text](url)
|
||||
while True:
|
||||
start_bracket = line.find('[')
|
||||
if start_bracket == -1:
|
||||
break # No more links in this line
|
||||
|
||||
end_bracket = line.find(']', start_bracket)
|
||||
if end_bracket == -1:
|
||||
break # Malformed link; exit loop
|
||||
|
||||
start_parenthesis = line.find('(', end_bracket)
|
||||
if start_parenthesis == -1:
|
||||
break # Malformed link; exit loop
|
||||
|
||||
end_parenthesis = line.find(')', start_parenthesis)
|
||||
if end_parenthesis == -1:
|
||||
break # Malformed link; exit loop
|
||||
|
||||
link_text = line[start_bracket + 1:end_bracket]
|
||||
url = line[start_parenthesis + 1:end_parenthesis]
|
||||
|
||||
# Add footnote
|
||||
footnotes.append(f" [{footnote_counter}]: {link_text}{url}")
|
||||
footnote_marker = f"{link_text}[{footnote_counter}]"
|
||||
footnote_counter += 1
|
||||
|
||||
# Replace link with footnote marker
|
||||
line = line[:start_bracket] + footnote_marker + line[end_parenthesis + 1:]
|
||||
|
||||
# Remove Markdown header markers
|
||||
if line.startswith('#'):
|
||||
line = line.lstrip('# ').strip()
|
||||
|
||||
output_lines.append(line.strip())
|
||||
|
||||
# Combine output lines and footnotes
|
||||
output_text = "\n".join(output_lines).strip()
|
||||
footnotes_text = "\n".join(footnotes)
|
||||
|
||||
return f"{output_text}\n\n{footnotes_text}" if footnotes else output_text
|
79
rivista/gmi/post.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#from markdown_text_clean import clean_text
|
||||
#from md2gemini import md2gemini
|
||||
#from rivista.gmi.markdown import GmiMarkdown
|
||||
from rivista.html.gmi import HtmlGmi
|
||||
from rivista.markdown.html import MarkdownHtml
|
||||
|
||||
class GmiPost:
|
||||
|
||||
def generate_gmi(atom: dict):
|
||||
"""Generate an Gemini document."""
|
||||
atom_title = atom['title']
|
||||
atom_subtitle = atom['subtitle']
|
||||
atom_header = f'# {atom_title}\n### {atom_subtitle}\n\n'
|
||||
atom_items = []
|
||||
for item in atom['items']:
|
||||
item_title = item['title']
|
||||
item_published = item['published']
|
||||
item_updated = item['updated']
|
||||
item_contents = ''
|
||||
for content in item['contents']:
|
||||
match content['type']:
|
||||
case 'text':
|
||||
content_html = MarkdownHtml.convert_to_html(content['text'])
|
||||
content_text = HtmlGmi.convert_to_gmi(content_html)
|
||||
#content_text = GmiMarkdown.convert_to_gmi(content['text'])
|
||||
#content_text = md2gemini(content['text'], links="at-end")
|
||||
#content_text = clean_text(content['text'])
|
||||
item_contents += f'\n{content_text}\n'
|
||||
case _ if content['type'] in ('html', 'xhtml'):
|
||||
content_text = HtmlGmi.convert_to_gmi(content['text'])
|
||||
item_contents += f'\n{content_text}\n'
|
||||
case _:
|
||||
content_text = content['text']
|
||||
item_contents += f'\n```\n{content_text}\n```\n'
|
||||
links = item['links'] if 'links' in item else None
|
||||
item_links = ''
|
||||
if links:
|
||||
item_links = '\n### Related resources\n\n'
|
||||
for link in links:
|
||||
link_href = link['href']
|
||||
link_rel = link['rel']
|
||||
link_type = link['type']
|
||||
if link_type:
|
||||
item_links += f'=> {link_href} {link_rel} ({link_type})\n'
|
||||
else:
|
||||
item_links += f'=> {link_href} {link_rel}\n'
|
||||
categories = item['categories'] if 'categories' in item else None
|
||||
item_categories = ''
|
||||
if categories:
|
||||
item_categories = '\n### Categories\n\n'
|
||||
for category in categories:
|
||||
item_categories += f'{category}, '
|
||||
item_categories = item_categories[0:len(item_categories)-2] + '.\n'
|
||||
authors = item['authors'] if 'authors' in item else None
|
||||
item_authors = ''
|
||||
if authors:
|
||||
item_authors = '\n### Authors\n\n'
|
||||
for author in authors:
|
||||
author_text = author['name'] or author['uri'] or author['email']
|
||||
if 'email' in author and author['email']:
|
||||
item_author_email = 'mailto:' + author['email']
|
||||
item_authors += f'=> {author_text} {item_author_email}\n'
|
||||
elif 'uri' in author and author['uri']:
|
||||
item_author_uri = author['uri']
|
||||
item_authors += f'=> {author_text} {item_author_uri}\n'
|
||||
else:
|
||||
item_authors += f'{author_text}\n'
|
||||
atom_items. append(f'\n## {item_title}\n\n' +
|
||||
f'Published: {item_published}\n' +
|
||||
f'Updated: {item_updated}\n' +
|
||||
item_contents +
|
||||
item_links +
|
||||
item_categories +
|
||||
item_authors)
|
||||
gmi_text = atom_header + '\n'.join(atom_items)
|
||||
return gmi_text
|
51
rivista/html/gmi.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
class HtmlGmi:
|
||||
|
||||
def convert_to_gmi(html_content):
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
footnotes = ['### References\n']
|
||||
footnote_counter = 1
|
||||
|
||||
# Extract text and links
|
||||
for a in soup.find_all('a'):
|
||||
link_text = a.get_text()
|
||||
url = a['href']
|
||||
if not url.startswith('#') and not url.startswith('/'):
|
||||
url_modified = url.replace(' ', '%20')
|
||||
url_modified = url_modified.replace('(', '%28')
|
||||
url_modified = url_modified.replace(')', '%29')
|
||||
#footnotes.append(f"=> {url_modified} [{footnote_counter}]: {link_text}")
|
||||
footnotes.append(f"=> {url_modified} [{footnote_counter}]: {url}")
|
||||
footnote_marker = f"{link_text}[{footnote_counter}]"
|
||||
a.replace_with(footnote_marker)
|
||||
footnote_counter += 1
|
||||
|
||||
# Handle <code> tags
|
||||
for code in soup.find_all('code'):
|
||||
if code.string:
|
||||
original_text = code.get_text()
|
||||
modified_text = '`' + original_text + '`'
|
||||
code.string.replace_with(modified_text)
|
||||
|
||||
# Handle <pre> tags
|
||||
for pre in soup.find_all('pre'):
|
||||
pre.insert_before('\n```\n') # Add Markdown code block start
|
||||
pre.insert_after('\n```\n') # Add Markdown code block end
|
||||
|
||||
# Convert <ul> and <li> to Markdown bullet points
|
||||
for ul in soup.find_all('ul'):
|
||||
for li in ul.find_all('li'):
|
||||
if li.string:
|
||||
original_text = li.get_text()
|
||||
modified_text = '- ' + original_text
|
||||
li.string.replace_with(modified_text)
|
||||
|
||||
# Get the text without HTML tags
|
||||
text = soup.get_text().strip().replace('#', '⋕')
|
||||
|
||||
# Combine text with footnotes
|
||||
return f"{text}\n\n" + "\n".join(footnotes) if footnotes else text
|
215
rivista/http/instance.py
Normal file
|
@ -0,0 +1,215 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import os
|
||||
from rivista.config import Cache, Data, Settings
|
||||
from rivista.gmi.post import GmiPost
|
||||
from rivista.json.index import JsonIndex
|
||||
from rivista.xml.atom import XmlAtom
|
||||
from rivista.xml.opml import XmlOpml
|
||||
from rivista.xml.xhtml import XmlXhtml
|
||||
from rivista.xml.xslt import XmlXslt
|
||||
from rivista.xmpp.instance import XmppInstance
|
||||
from rivista.xmpp.utilities import XmppUtilities
|
||||
from rivista.xmpp.xep_0060 import XmppXep0060
|
||||
|
||||
class HttpInstance:
|
||||
def __init__(self):
|
||||
|
||||
directory_settings = Settings.get_directory()
|
||||
filename_settings = os.path.join(directory_settings, 'settings.toml')
|
||||
credentials = Settings.get_setting(filename_settings, 'account')
|
||||
settings = Settings.get_setting(filename_settings, 'settings')
|
||||
default = Settings.get_setting(filename_settings, 'default')
|
||||
operator = Settings.get_setting(filename_settings, 'settings')['operator']
|
||||
|
||||
directory_data = Data.get_directory()
|
||||
directory_data_css = os.path.join(directory_data, 'css')
|
||||
directory_data_graphic = os.path.join(directory_data, 'graphic')
|
||||
directory_data_script = os.path.join(directory_data, 'script')
|
||||
directory_data_xsl = os.path.join(directory_data, 'xsl')
|
||||
filename_favicon = os.path.join(directory_data, 'img', 'favicon.ico')
|
||||
|
||||
directory_cache = Cache.get_directory()
|
||||
directory_cache_json = os.path.join(directory_cache, 'json')
|
||||
|
||||
self.app = FastAPI()
|
||||
|
||||
# Mount static graphic, script and stylesheet directories
|
||||
self.app.mount("/css", StaticFiles(directory=directory_data_css), name="css")
|
||||
self.app.mount("/data", StaticFiles(directory=directory_cache_json), name="data")
|
||||
self.app.mount("/graphic", StaticFiles(directory=directory_data_graphic), name="graphic")
|
||||
self.app.mount("/script", StaticFiles(directory=directory_data_script), name="script")
|
||||
self.app.mount("/xsl", StaticFiles(directory=directory_data_xsl), name="xsl")
|
||||
|
||||
@self.app.get('/favicon.ico', include_in_schema=False)
|
||||
async def favicon():
|
||||
return FileResponse(filename_favicon)
|
||||
|
||||
@self.app.route('/')
|
||||
@self.app.get('/opml')
|
||||
async def view_pubsub_nodes(request: Request):
|
||||
xmpp = XmppInstance(credentials['xmpp'], credentials['pass'])
|
||||
# xmpp.connect()
|
||||
|
||||
pubsub = request.query_params.get('pubsub', '')
|
||||
result = None
|
||||
if settings['service']:
|
||||
if not settings['include'] or settings['include'] in pubsub:
|
||||
if pubsub:
|
||||
iq = await XmppXep0060.get_nodes(xmpp, pubsub)
|
||||
if iq:
|
||||
link = 'xmpp:{pubsub}'.format(pubsub=pubsub)
|
||||
xml_opml = XmlOpml.generate_opml(iq)
|
||||
result = XmlXslt.append_stylesheet(xml_opml, 'opml')
|
||||
else:
|
||||
text = 'Please ensure that PubSub "{}" (Jabber ID) is valid and accessible.'.format(pubsub)
|
||||
xml_atom = XmlAtom.error_message(text)
|
||||
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
|
||||
else:
|
||||
text = 'The given domain {} is not allowed.'.format(pubsub)
|
||||
xml_atom = XmlAtom.error_message(text)
|
||||
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
|
||||
if not result:
|
||||
if default['pubsub']:
|
||||
if not pubsub:
|
||||
pubsub = default['pubsub']
|
||||
iq = await XmppXep0060.get_nodes(xmpp, pubsub)
|
||||
link = 'xmpp:{pubsub}'.format(pubsub=pubsub)
|
||||
xml_opml = XmlOpml.generate_opml(iq)
|
||||
result = XmlXslt.append_stylesheet(xml_opml, 'opml')
|
||||
elif not settings['service']:
|
||||
pubsub = default['pubsub']
|
||||
link = 'xmpp:{pubsub}'.format(pubsub=pubsub)
|
||||
xml_opml = XmlOpml.generate_opml(iq)
|
||||
result = XmlXslt.append_stylesheet(xml_opml, 'opml')
|
||||
else:
|
||||
text = 'Please contact the administrator and ask him to set default PubSub and Node ID.'
|
||||
xml_atom = XmlAtom.error_message(text)
|
||||
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
|
||||
xmpp.disconnect()
|
||||
response = Response(content=result, media_type="application/xml")
|
||||
return response
|
||||
|
||||
@self.app.get('/atom')
|
||||
async def view_node_items(request: Request):
|
||||
xmpp = XmppInstance(credentials['xmpp'], credentials['pass'])
|
||||
# xmpp.connect()
|
||||
|
||||
pubsub = request.query_params.get('pubsub', '')
|
||||
node = request.query_params.get('node', '')
|
||||
item_id = request.query_params.get('item', '')
|
||||
result = None
|
||||
if settings['service']:
|
||||
if not settings['include'] or settings['include'] in pubsub:
|
||||
if pubsub and node and item_id:
|
||||
iq = await XmppXep0060.get_node_item(xmpp, pubsub, node, item_id)
|
||||
if iq:
|
||||
link = XmppUtilities.form_an_item_link(pubsub, node, item_id)
|
||||
if 'urn:xmpp:microblog:0:comments/' in node:
|
||||
atom = XmlAtom.extract_atom(iq)
|
||||
xml_atom = XmlAtom.generate_atom_comment(atom, pubsub, node, link)
|
||||
else:
|
||||
atom = XmlAtom.extract_atom(iq)
|
||||
xml_atom = XmlAtom.generate_atom_post(atom, pubsub, node, link)
|
||||
gmi_text = GmiPost.generate_gmi(atom)
|
||||
iq = await XmppXep0060.get_node_items(xmpp, pubsub, node)
|
||||
if not '/' in node:
|
||||
if iq:
|
||||
JsonIndex.generate_json(iq, directory_cache_json)
|
||||
else:
|
||||
json_data = [{'title' : 'Error retrieving node items.',
|
||||
'link' : ('javascript:alert("Rivista has experienced an error '
|
||||
'while attempting to retrieve the list of items for '
|
||||
'Node {} of PubSub {}.")')
|
||||
.format(node, pubsub)},
|
||||
{'title' : 'Contact the operator.',
|
||||
'link' : ('xmpp:{}?message;subject=Rivista;body=Greetings! '
|
||||
'I am contacting you to inform you that there is an error listing '
|
||||
'node items for Node {} on PubSub {}.').format(operator, node, pubsub)}]
|
||||
filename = os.path.join(directory_cache_json, 'json', f'{node}.json')
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(json_data, f, ensure_ascii=False, indent=4)
|
||||
else:
|
||||
text = 'Please ensure that PubSub node "{}" and item "{}" are valid and accessible.'.format(node, item_id)
|
||||
xml_atom = XmlAtom.error_message(text)
|
||||
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
|
||||
|
||||
# try:
|
||||
# iq = await XmppXep0060.get_node_items(xmpp, pubsub, node)
|
||||
# JsonIndex.generate_json(iq, node)
|
||||
# except:
|
||||
# json_data = [{'title' : 'Timeout retrieving node items from {}'.format(node),
|
||||
# 'link' : 'xmpp:{}?message'.format(operator)}]
|
||||
# filename = 'data/{}.json'.format(node)
|
||||
# with open(filename, 'w', encoding='utf-8') as f:
|
||||
# json.dump(json_data, f, ensure_ascii=False, indent=4)
|
||||
elif pubsub and node:
|
||||
iq = await XmppXep0060.get_node_items(xmpp, pubsub, node)
|
||||
if iq:
|
||||
link = XmppUtilities.form_a_node_link(pubsub, node)
|
||||
if 'urn:xmpp:microblog:0:comments/' in node:
|
||||
atom = XmlAtom.extract_atom(iq)
|
||||
xml_atom = XmlAtom.generate_atom_comment(atom, pubsub, node, link)
|
||||
else:
|
||||
atom = XmlAtom.extract_atom(iq)
|
||||
xml_atom = XmlAtom.generate_atom_post(atom, pubsub, node, link)
|
||||
gmi_text = GmiPost.generate_gmi(atom)
|
||||
else:
|
||||
text = 'Please ensure that PubSub node "{}" is valid and accessible.'.format(node)
|
||||
xml_atom = XmlAtom.error_message(text)
|
||||
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
|
||||
elif pubsub:
|
||||
text = 'Node parameter is missing.'
|
||||
xml_atom = XmlAtom.error_message(text)
|
||||
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
|
||||
elif node:
|
||||
text = 'PubSub parameter is missing.'
|
||||
xml_atom = XmlAtom.error_message(text)
|
||||
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
|
||||
# else:
|
||||
# text = ('Mandatory parameter PubSub and '
|
||||
# 'optional parameter Node are missing.')
|
||||
# xml_atom = XmlAtom.error_message(text)
|
||||
# result = XmlXslt.append_stylesheet(xml_atom, 'atom')
|
||||
else:
|
||||
text = 'The given domain {} is not allowed.'.format(pubsub)
|
||||
xml_atom = XmlAtom.error_message(text)
|
||||
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
|
||||
if not result:
|
||||
if default['pubsub'] and default['nodeid']:
|
||||
if not pubsub and not node:
|
||||
pubsub = default['pubsub']
|
||||
node = default['nodeid']
|
||||
iq = await XmppXep0060.get_node_items(xmpp, pubsub, node)
|
||||
if iq:
|
||||
link = XmppUtilities.form_a_node_link(pubsub, node)
|
||||
atom = XmlAtom.extract_atom(iq)
|
||||
xml_atom = XmlAtom.generate_atom_post(atom, pubsub, node, link)
|
||||
gmi_text = GmiPost.generate_gmi(atom)
|
||||
else:
|
||||
text = 'Please ensure that PubSub node "{}" is valid and accessible.'.format(node)
|
||||
xml_atom = XmlAtom.error_message(text)
|
||||
elif not settings['service']:
|
||||
pubsub = default['pubsub']
|
||||
node = default['nodeid']
|
||||
iq = await XmppXep0060.get_node_items(xmpp, pubsub, node)
|
||||
if iq:
|
||||
link = XmppUtilities.form_a_node_link(pubsub, node)
|
||||
atom = XmlAtom.extract_atom(iq)
|
||||
xml_atom = XmlAtom.generate_atom_post(atom, pubsub, node, link)
|
||||
gmi_text = GmiPost.generate_gmi(atom)
|
||||
else:
|
||||
text = 'Please ensure that PubSub node "{}" is valid and accessible.'.format(node)
|
||||
xml_atom = XmlAtom.error_message(text)
|
||||
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
|
||||
else:
|
||||
text = 'Please contact the administrator and ask him to set default PubSub and Node ID.'
|
||||
xml_atom = XmlAtom.error_message(text)
|
||||
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
|
||||
xmpp.disconnect()
|
||||
response = Response(content=result, media_type="application/xml")
|
||||
return response
|
38
rivista/json/index.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import os
|
||||
from rivista.xmpp.utilities import XmppUtilities
|
||||
|
||||
class JsonIndex:
|
||||
|
||||
def generate_json(iq, directory_cache_json):
|
||||
"""Create a JSON file from node items."""
|
||||
json_data = []
|
||||
pubsub = iq['from'].bare
|
||||
node = iq['pubsub']['items']['node']
|
||||
entries = iq['pubsub']['items']
|
||||
for entry in entries:
|
||||
item_id = entry['id']
|
||||
item_payload = entry['payload']
|
||||
namespace = '{http://www.w3.org/2005/Atom}'
|
||||
title = item_payload.find(namespace + 'title')
|
||||
title_text = '*** No Title ***' if title == None else title.text
|
||||
# updated = item.find(namespace + 'updated')
|
||||
# updated = None if updated == None else updated.text
|
||||
# if updated: updated = datetime.datetime.fromisoformat(updated)
|
||||
link_href = XmppUtilities.form_an_item_link(pubsub, node, item_id)
|
||||
# link = item.find(namespace + 'link')
|
||||
# link_href = '' if link == None else link.attrib['href']
|
||||
json_data_entry = {'title' : title_text,
|
||||
'link' : link_href}
|
||||
json_data.append(json_data_entry)
|
||||
#if len(json_data) > 6: break
|
||||
directory = os.path.join(directory_cache_json, pubsub)
|
||||
if not os.path.exists(directory):
|
||||
os.mkdir(directory)
|
||||
filename = os.path.join(directory_cache_json, pubsub, f'{node}.json')
|
||||
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(json_data, f, ensure_ascii=False, indent=4)
|
11
rivista/markdown/html.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import markdown
|
||||
|
||||
class MarkdownHtml:
|
||||
|
||||
def convert_to_html(markdown_content):
|
||||
# Convert Markdown to HTML
|
||||
html_content = markdown.markdown(markdown_content)
|
||||
return html_content
|
Before Width: | Height: | Size: 599 KiB After Width: | Height: | Size: 599 KiB |
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 154 KiB |
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
22
rivista/utilities.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import tomli_w
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except:
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
class Toml:
|
||||
|
||||
def open_file_toml(filename: str) -> dict:
|
||||
with open(filename, mode="rb") as fn:
|
||||
data = tomllib.load(fn)
|
||||
return data
|
||||
|
||||
def save_to_toml(filename: str, data: dict) -> None:
|
||||
with open(filename, 'w') as fn:
|
||||
data_as_string = tomli_w.dumps(data)
|
||||
fn.write(data_as_string)
|
2
rivista/version.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
__version__ = '0.1'
|
||||
__version_info__ = (0, 1)
|
251
rivista/xml/atom.py
Normal file
|
@ -0,0 +1,251 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from rivista.xmpp.utilities import XmppUtilities
|
||||
from slixmpp.stanza.iq import Iq
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
class XmlAtom:
|
||||
|
||||
def error_message(text):
|
||||
"""Error message in RFC 4287: The Atom Syndication Format."""
|
||||
title = 'Rivista'
|
||||
subtitle = 'XMPP Journal Publisher'
|
||||
description = ('This is a syndication feed generated with Rivista, an XMPP '
|
||||
'Journal Publisher, which conveys XEP-0060: Publish-'
|
||||
'Subscribe nodes to standard RFC 4287: The Atom Syndication '
|
||||
'Format.')
|
||||
language = 'en'
|
||||
feed = ET.Element("feed")
|
||||
feed.set('xmlns', 'http://www.w3.org/2005/Atom')
|
||||
ET.SubElement(feed, 'title', {'type': 'text'}).text = title
|
||||
ET.SubElement(feed, 'subtitle', {'type': 'text'}).text = subtitle
|
||||
ET.SubElement(feed, 'author', {'name':'Rivista','email':'rivista@schimon.i2p'})
|
||||
ET.SubElement(feed, 'generator', {
|
||||
'uri': 'https://git.xmpp-it.net/sch/Rivista',
|
||||
'version': '0.1'}).text = 'Rivista XJP'
|
||||
ET.SubElement(feed, 'updated').text = datetime.datetime.now(datetime.UTC).isoformat()
|
||||
entry = ET.SubElement(feed, 'entry')
|
||||
ET.SubElement(entry, 'title').text = 'Error'
|
||||
ET.SubElement(entry, 'id').text = 'rivista-error'
|
||||
ET.SubElement(entry, 'updated').text = datetime.datetime.now(datetime.UTC).isoformat()
|
||||
ET.SubElement(entry, 'published').text = datetime.datetime.now(datetime.UTC).isoformat()
|
||||
# ET.SubElement(entry, 'summary', {'type': summary_type_text}).text = summary_text
|
||||
ET.SubElement(entry, 'content', {'type': 'text'}).text = text
|
||||
return ET.tostring(feed, encoding='unicode')
|
||||
|
||||
def extract_atom(iq: Iq):
|
||||
"""Extract data from an Atom Syndication Format (RFC 4287) of a Publish-Subscribe (XEP-0060) node item."""
|
||||
jid = iq['from'].bare
|
||||
node = iq['pubsub']['items']['node']
|
||||
atom = {}
|
||||
atom['title'] = jid
|
||||
atom['subtitle'] = node
|
||||
atom['language'] = iq['pubsub']['items']['lang']
|
||||
atom['items'] = []
|
||||
items = iq['pubsub']['items']
|
||||
for item in list(items)[::-1]:
|
||||
atom_item = {}
|
||||
item_payload = item['payload']
|
||||
namespace = '{http://www.w3.org/2005/Atom}'
|
||||
title = item_payload.find(namespace + 'title')
|
||||
links = item_payload.find(namespace + 'link')
|
||||
if (not isinstance(title, ET.Element) and
|
||||
not isinstance(links, ET.Element)): continue
|
||||
title_text = 'No title' if title == None else title.text
|
||||
atom_item['title'] = title_text
|
||||
if isinstance(links, ET.Element):
|
||||
atom_item['links'] = []
|
||||
for link in item_payload.findall(namespace + 'link'):
|
||||
link_href = link.attrib['href'] if 'href' in link.attrib else ''
|
||||
link_type = link.attrib['type'] if 'type' in link.attrib else ''
|
||||
link_rel = link.attrib['rel'] if 'rel' in link.attrib else ''
|
||||
atom_item['links'].append({'href': link_href,
|
||||
'rel': link_rel,
|
||||
'type': link_type})
|
||||
contents = item_payload.find(namespace + 'content')
|
||||
atom_item['contents'] = []
|
||||
if isinstance(contents, ET.Element):
|
||||
for content in item_payload.findall(namespace + 'content'):
|
||||
if not content.text: continue
|
||||
content_text = content.text
|
||||
content_type = content.attrib['type'] if 'type' in content.attrib else 'html'
|
||||
content_type_text = 'html' if 'html' in content_type else 'text'
|
||||
atom_item['contents'].append({'text' : content_text,
|
||||
'type' : content_type_text})
|
||||
else:
|
||||
summary = item_payload.find(namespace + 'summary')
|
||||
summary_text = summary.text if summary else None
|
||||
if summary_text:
|
||||
summary_type = summary.attrib['type'] if 'type' in summary.attrib else 'html'
|
||||
summary_type_text = 'html' if 'html' in summary_type else 'text'
|
||||
atom_item['contents'].append(summary_text)
|
||||
# else:
|
||||
# atom_item['contents'].append('No content.')
|
||||
published = item_payload.find(namespace + 'published')
|
||||
published_text = '' if published == None else published.text
|
||||
atom_item['published'] = published_text
|
||||
updated = item_payload.find(namespace + 'updated')
|
||||
updated_text = '' if updated == None else updated.text
|
||||
atom_item['updated'] = updated_text
|
||||
atom_item['authors'] = []
|
||||
authors = item_payload.find(namespace + 'author')
|
||||
if isinstance(authors, ET.Element):
|
||||
for author in item_payload.findall(namespace + 'author'):
|
||||
atom_item_author = {}
|
||||
author_email = author.find(namespace + 'email')
|
||||
if author_email is not None:
|
||||
author_email_text = author_email.text
|
||||
if author_email_text:
|
||||
atom_item_author['email'] = author_email_text
|
||||
else:
|
||||
author_email_text = None
|
||||
author_uri = author.find(namespace + 'uri')
|
||||
if author_uri is not None:
|
||||
author_uri_text = author_uri.text
|
||||
if author_uri_text:
|
||||
atom_item_author['uri'] = author_uri_text
|
||||
else:
|
||||
author_uri_text = None
|
||||
author_name = author.find(namespace + 'name')
|
||||
if author_name is not None and author_name.text:
|
||||
author_name_text = author_name.text
|
||||
else:
|
||||
author_name_text = author_uri_text or author_email_text
|
||||
atom_item_author['name'] = author_name_text
|
||||
atom_item['authors'].append(atom_item_author)
|
||||
categories = item_payload.find(namespace + 'category')
|
||||
atom_item['categories'] = []
|
||||
if isinstance(categories, ET.Element):
|
||||
for category in item_payload.findall(namespace + 'category'):
|
||||
if 'term' in category.attrib and category.attrib['term']:
|
||||
category_term = category.attrib['term']
|
||||
atom_item['categories'].append(category_term)
|
||||
identifier = item_payload.find(namespace + 'id')
|
||||
if identifier is not None and identifier.attrib: print(identifier.attrib)
|
||||
identifier_text = item['id'] if identifier == None else identifier.text
|
||||
atom_item['id'] = identifier_text
|
||||
#atom_item['id'] = item['id']
|
||||
atom['items'].append(atom_item)
|
||||
return atom
|
||||
|
||||
# generate_rfc_4287
|
||||
def generate_atom_post(atom: dict, pubsub: str, node: str, link: str):
|
||||
"""Generate an Atom Syndication Format (RFC 4287) from a Publish-Subscribe (XEP-0060) node items."""
|
||||
# link = XmppUtilities.form_a_node_link(pubsub, node)
|
||||
# subtitle = 'XMPP PubSub Syndication Feed'
|
||||
description = ('This is a syndication feed generated with Rivista, an XMPP '
|
||||
'Journal Publisher, which conveys XEP-0060: Publish-'
|
||||
'Subscribe nodes to standard RFC 4287: The Atom Syndication '
|
||||
'Format.')
|
||||
e_feed = ET.Element("feed")
|
||||
e_feed.set('xmlns', 'http://www.w3.org/2005/Atom')
|
||||
ET.SubElement(e_feed, 'title', {'type': 'text'}).text = atom['title']
|
||||
ET.SubElement(e_feed, 'subtitle', {'type': 'text'}).text = atom['subtitle']
|
||||
ET.SubElement(e_feed, 'link', {'rel': 'self', 'href': link})
|
||||
ET.SubElement(e_feed, 'generator', {
|
||||
'uri': 'https://git.xmpp-it.net/sch/Rivista',
|
||||
'version': '0.1'}).text = 'Rivista XJP'
|
||||
ET.SubElement(e_feed, 'updated').text = datetime.datetime.now(datetime.UTC).isoformat()
|
||||
for item in atom['items']:
|
||||
e_entry = ET.SubElement(e_feed, 'entry')
|
||||
ET.SubElement(e_entry, 'title').text = item['title']
|
||||
links = item['links'] if 'links' in item else None
|
||||
if links:
|
||||
for link in links:
|
||||
ET.SubElement(e_entry, 'link', {'href': link['href'],
|
||||
'rel': link['rel'],
|
||||
'type': link['type']})
|
||||
else:
|
||||
# NOTE What does this instruction line do?
|
||||
ET.SubElement(e_entry, 'content', {'href': ''})
|
||||
link_xmpp = XmppUtilities.form_an_item_link(pubsub, node, item['id'])
|
||||
ET.SubElement(e_entry, 'link', {'href': link_xmpp,
|
||||
'rel': 'alternate',
|
||||
'type': 'x-scheme-handler/xmpp'})
|
||||
contents = item['contents'] if 'contents' in item else None
|
||||
if contents:
|
||||
for content in contents:
|
||||
ET.SubElement(e_entry, 'content', {'type': content['type']}).text = content['text']
|
||||
else:
|
||||
ET.SubElement(e_entry, 'content').text = 'No content.'
|
||||
ET.SubElement(e_entry, 'published').text = item['published']
|
||||
ET.SubElement(e_entry, 'updated').text = item['updated']
|
||||
authors = item['authors'] if 'authors' in item else None
|
||||
if authors:
|
||||
for author in authors:
|
||||
e_author = ET.SubElement(e_entry, 'author')
|
||||
if 'email' in author and author['email']:
|
||||
ET.SubElement(e_author, 'email').text = author['email']
|
||||
if 'uri' in author and author['uri']:
|
||||
ET.SubElement(e_entry, 'uri').text = author['uri']
|
||||
ET.SubElement(e_author, 'uri').text = author['uri']
|
||||
ET.SubElement(e_author, 'name').text = author['name'] or author['uri'] or author['email']
|
||||
categories = item['categories']
|
||||
if categories:
|
||||
for category in categories:
|
||||
ET.SubElement(e_entry, 'category', {'term': category})
|
||||
|
||||
ET.SubElement(e_entry, 'id').text = item['id']
|
||||
return ET.tostring(e_feed, encoding='unicode')
|
||||
|
||||
# generate_rfc_4287
|
||||
def generate_atom_comment(atom: dict, pubsub: str, node: str, link: str):
|
||||
"""Generate an Atom Syndication Format (RFC 4287) from a Publish-Subscribe (XEP-0060) node items."""
|
||||
# link = XmppUtilities.form_a_node_link(pubsub, node)
|
||||
# subtitle = 'XMPP PubSub Syndication Feed'
|
||||
description = ('This is a syndication feed generated with Rivista, an XMPP '
|
||||
'Journal Publisher, which conveys XEP-0060: Publish-'
|
||||
'Subscribe nodes to standard RFC 4287: The Atom Syndication '
|
||||
'Format.')
|
||||
e_feed = ET.Element("feed")
|
||||
e_feed.set('xmlns', 'http://www.w3.org/2005/Atom')
|
||||
ET.SubElement(e_feed, 'title', {'type': 'text'}).text = atom['title']
|
||||
ET.SubElement(e_feed, 'subtitle', {'type': 'text'}).text = atom['subtitle']
|
||||
ET.SubElement(e_feed, 'link', {'rel': 'self', 'href': link})
|
||||
ET.SubElement(e_feed, 'generator', {
|
||||
'uri': 'https://git.xmpp-it.net/sch/Rivista',
|
||||
'version': '0.1'}).text = 'Rivista XJP'
|
||||
ET.SubElement(e_feed, 'updated').text = datetime.datetime.now(datetime.UTC).isoformat()
|
||||
for item in atom['items']:
|
||||
e_entry = ET.SubElement(e_feed, 'entry')
|
||||
ET.SubElement(e_entry, 'title').text = item['authors'][0]['name']
|
||||
links = item['links'] if 'links' in item else None
|
||||
if links:
|
||||
for link in links:
|
||||
ET.SubElement(e_entry, 'link', {'href': link['href'],
|
||||
'rel': link['rel'],
|
||||
'type': link['type']})
|
||||
else:
|
||||
# NOTE What does this instruction line do?
|
||||
ET.SubElement(e_entry, 'content', {'href': ''})
|
||||
link_xmpp = XmppUtilities.form_an_item_link(pubsub, node, item['id'])
|
||||
ET.SubElement(e_entry, 'link', {'href': link_xmpp,
|
||||
'rel': 'alternate',
|
||||
'type': 'x-scheme-handler/xmpp'})
|
||||
contents = item['contents'] if 'contents' in item else None
|
||||
if contents:
|
||||
for content in contents:
|
||||
ET.SubElement(e_entry, 'content', {'type': content['type']}).text = content['text']
|
||||
else:
|
||||
ET.SubElement(e_entry, 'content').text = 'No content.'
|
||||
ET.SubElement(e_entry, 'published').text = item['published']
|
||||
ET.SubElement(e_entry, 'updated').text = item['updated']
|
||||
authors = item['authors'] if 'authors' in item else None
|
||||
if authors:
|
||||
for author in authors:
|
||||
e_author = ET.SubElement(e_entry, 'author')
|
||||
if 'email' in author and author['email']:
|
||||
ET.SubElement(e_author, 'email').text = author['email']
|
||||
if 'uri' in author and author['uri']:
|
||||
ET.SubElement(e_entry, 'uri').text = author['uri']
|
||||
ET.SubElement(e_author, 'uri').text = author['uri']
|
||||
ET.SubElement(e_author, 'name').text = author['name'] or author['uri'] or author['email']
|
||||
categories = item['categories']
|
||||
if categories:
|
||||
for category in categories:
|
||||
ET.SubElement(e_entry, 'category', {'term': category})
|
||||
|
||||
ET.SubElement(e_entry, 'id').text = item['id']
|
||||
return ET.tostring(e_feed, encoding='unicode')
|
32
rivista/xml/opml.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from rivista.xmpp.utilities import XmppUtilities
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
class XmlOpml:
|
||||
|
||||
def generate_opml(iq):
|
||||
"""Generate an OPML Collection document."""
|
||||
pubsub = iq['from'].bare
|
||||
items = iq['disco_items']['items']
|
||||
opml = ET.Element("opml")
|
||||
opml.set("version", "1.0")
|
||||
head = ET.SubElement(opml, "head")
|
||||
ET.SubElement(head, "title").text = 'An OPML of ' + pubsub
|
||||
ET.SubElement(head, "description").text = (
|
||||
"PubSub Nodes of {}").format(pubsub)
|
||||
ET.SubElement(head, "generator").text = 'Rivista'
|
||||
ET.SubElement(head, "urlPublic").text = 'https://git.xmpp-it.net/sch/Rivista'
|
||||
time_stamp = datetime.datetime.now(datetime.UTC).isoformat()
|
||||
ET.SubElement(head, "dateCreated").text = time_stamp
|
||||
ET.SubElement(head, "dateModified").text = time_stamp
|
||||
body = ET.SubElement(opml, "body")
|
||||
for item in items:
|
||||
pubsub, node, title = item
|
||||
uri = XmppUtilities.form_a_node_link(pubsub, node)
|
||||
outline = ET.SubElement(body, "outline")
|
||||
outline.set("text", title or node)
|
||||
outline.set("xmlUrl", uri)
|
||||
return ET.tostring(opml, encoding='unicode')
|
62
rivista/xml/xhtml.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
class XmlXhtml:
|
||||
|
||||
def generate_xhtml(atom: dict):
|
||||
"""Generate an XHTML document."""
|
||||
e_html = ET.Element('html')
|
||||
e_html.set('xmlns', 'http://www.w3.org/1999/xhtml')
|
||||
e_head = ET.SubElement(e_html, 'head')
|
||||
ET.SubElement(e_head, 'title').text = atom['title']
|
||||
ET.SubElement(e_head, 'link', {'rel': 'stylesheet',
|
||||
'href': 'pubsub.css'})
|
||||
e_body = ET.SubElement(e_html, "body")
|
||||
ET.SubElement(e_body, "h1").text = atom['title']
|
||||
ET.SubElement(e_body, "h2").text = atom['subtitle']
|
||||
for item in atom['items']:
|
||||
item_id = item['id']
|
||||
title = item['title']
|
||||
links = item['links']
|
||||
e_article = ET.SubElement(e_body, 'article')
|
||||
e_title = ET.SubElement(e_article, 'h3')
|
||||
e_title.text = item['title']
|
||||
e_title.set('id', item['id'])
|
||||
e_date = ET.SubElement(e_article, 'h4')
|
||||
e_date.text = item['published']
|
||||
e_date.set('title', 'Updated: ' + item['updated'])
|
||||
authors = item['authors']
|
||||
if authors:
|
||||
e_authors = ET.SubElement(e_article, "dl")
|
||||
ET.SubElement(e_authors, "dt").text = 'Authors'
|
||||
for author in authors:
|
||||
e_dd = ET.SubElement(e_authors, 'dd')
|
||||
e_author = ET.SubElement(e_dd, 'a')
|
||||
e_author.text = author['name'] or author['uri'] or author['email']
|
||||
if 'email' in author and author['email']:
|
||||
e_author.set('href', 'mailto:' + author['email'])
|
||||
elif 'uri' in author and author['uri']:
|
||||
e_author.set('href', author['uri'])
|
||||
for content in item['contents']:
|
||||
ET.SubElement(e_article, 'p', {'type': content['type']}).text = content['text']
|
||||
if links:
|
||||
e_links = ET.SubElement(e_article, "dl")
|
||||
e_links.set('class', 'links')
|
||||
ET.SubElement(e_links, "dt").text = 'Links'
|
||||
for link in links:
|
||||
e_dd = ET.SubElement(e_links, 'dd')
|
||||
e_link = ET.SubElement(e_dd, 'a')
|
||||
e_link.set('href', link['href'])
|
||||
e_link.text = link['rel']
|
||||
if link['type']: ET.SubElement(e_dd, 'span').text = link['type']
|
||||
categories = item['categories']
|
||||
if categories:
|
||||
e_categories = ET.SubElement(e_article, "dl")
|
||||
e_categories.set('class', 'categories')
|
||||
ET.SubElement(e_categories, "dt").text = 'Categories'
|
||||
for category in categories:
|
||||
ET.SubElement(e_categories, 'dd').text = category
|
||||
return ET.tostring(e_html, encoding='unicode')
|
22
rivista/xml/xslt.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
class XmlXslt:
|
||||
|
||||
"""This is a patch function to append XSLT reference to XML."""
|
||||
"""Why is there no built-in function of ElementTree or LXML?"""
|
||||
def append_stylesheet(xml_data, type):
|
||||
# Register namespace in order to avoide ns0:
|
||||
if type == 'atom': ET.register_namespace('', 'http://www.w3.org/2005/Atom')
|
||||
# Load XML from string
|
||||
tree = ET.fromstring(xml_data)
|
||||
# The following direction removes the XML declaration
|
||||
xml_data_without_a_declaration = ET.tostring(tree, encoding='unicode')
|
||||
# Add XML declaration and stylesheet
|
||||
xml_data_declaration = (
|
||||
'<?xml version="1.0" encoding="utf-8"?>'
|
||||
'<?xml-stylesheet type="text/xsl" href="xsl/{}.xsl"?>'.format(type) +
|
||||
xml_data_without_a_declaration)
|
||||
return xml_data_declaration
|
11
rivista/xmpp/instance.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from slixmpp import ClientXMPP
|
||||
|
||||
class XmppInstance(ClientXMPP):
|
||||
def __init__(self, jid, password):
|
||||
super().__init__(jid, password)
|
||||
self.register_plugin('xep_0060')
|
||||
self.connect()
|
||||
# self.process(forever=False)
|
14
rivista/xmpp/utilities.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class XmppUtilities:
|
||||
|
||||
def form_a_node_link(pubsub, node):
|
||||
link = 'xmpp:{pubsub}?;node={node}'.format(pubsub=pubsub, node=node)
|
||||
return link
|
||||
|
||||
def form_an_item_link(pubsub, node, item_id):
|
||||
link = 'xmpp:{pubsub}?;node={node};item={item}'.format(
|
||||
pubsub=pubsub, node=node, item=item_id)
|
||||
return link
|
27
rivista/xmpp/xep_0060.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from slixmpp.exceptions import IqError, IqTimeout
|
||||
|
||||
class XmppXep0060:
|
||||
|
||||
async def get_node_item(self, pubsub, node, item_id):
|
||||
try:
|
||||
iq = await self.plugin['xep_0060'].get_item(pubsub, node, item_id, timeout=5)
|
||||
return iq
|
||||
except (IqError, IqTimeout) as e:
|
||||
print(e)
|
||||
|
||||
async def get_node_items(self, pubsub, node):
|
||||
try:
|
||||
iq = await self.plugin['xep_0060'].get_items(pubsub, node, timeout=5)
|
||||
return iq
|
||||
except (IqError, IqTimeout) as e:
|
||||
print(e)
|
||||
|
||||
async def get_nodes(self, pubsub):
|
||||
try:
|
||||
iq = await self.plugin['xep_0060'].get_nodes(pubsub, timeout=5)
|
||||
return iq
|
||||
except (IqError, IqTimeout) as e:
|
||||
print(e)
|
|
@ -1,92 +0,0 @@
|
|||
window.onload = function(){
|
||||
// Convert ISO8601 To UTC/
|
||||
for (let element of document.querySelectorAll('#articles > ul > li > div > h4, #feed > #header > h2#subtitle.date')) {
|
||||
let timeStamp = new Date(element.textContent);
|
||||
element.textContent = timeStamp.toUTCString();
|
||||
}
|
||||
// Parse Markdown/
|
||||
for (let element of document.querySelectorAll('#articles > ul > li > div > p')) {
|
||||
let markDown = element.textContent
|
||||
element.innerHTML = marked.parse(markDown);
|
||||
}
|
||||
// Display a selection of suggested software.
|
||||
const selection = {
|
||||
'akregator' : {
|
||||
'name' : 'Akregator',
|
||||
'image' : 'akregator.svg',
|
||||
'url' : 'https://apps.kde.org/akregator/'
|
||||
},
|
||||
'leechcraft' : {
|
||||
'name' : 'LeechCraft',
|
||||
'image' : 'leechcraft.png',
|
||||
'url' : 'https://leechcraft.org/'
|
||||
},
|
||||
'liferea' : {
|
||||
'name' : 'Liferea',
|
||||
'image' : 'liferea.svg',
|
||||
'url' : 'https://lzone.de/liferea/'
|
||||
},
|
||||
'raven' : {
|
||||
'name' : 'Raven Reader',
|
||||
'image' : 'raven.svg',
|
||||
'url' : 'https://ravenreader.app/'
|
||||
},
|
||||
'rssguard' : {
|
||||
'name' : 'RSS Guard',
|
||||
'image' : 'rssguard.png',
|
||||
'url' : 'https://github.com/martinrotter/rssguard'
|
||||
},
|
||||
'rssowl' : {
|
||||
'name' : 'RSSOwl',
|
||||
'image' : 'rssowl.svg',
|
||||
'url' : 'http://www.rssowl.org/'
|
||||
},
|
||||
'tickr' : {
|
||||
'name' : 'TICKR',
|
||||
'image' : 'tickr.png',
|
||||
'url' : 'https://www.open-tickr.net/'
|
||||
}
|
||||
}
|
||||
let selectionLink = document.querySelector('#selection-link');
|
||||
selectionLink.addEventListener ('click', function() {
|
||||
let elementDiv = document.createElement('div');
|
||||
elementDiv.id = 'selection-page';
|
||||
let elementH1 = document.createElement('h1');
|
||||
elementH1.textContent = 'Get A News Reader';
|
||||
elementDiv.appendChild(elementH1);
|
||||
let elementH2 = document.createElement('h2');
|
||||
elementH2.textContent = 'Install Feed Reader Apps For Desktop And Mobile';
|
||||
elementDiv.appendChild(elementH2);
|
||||
let elementH3 = document.createElement('h3');
|
||||
elementH3.textContent = '' +
|
||||
'This is a selection of desktop applications, mobile apps and online ' +
|
||||
'services for you to choose from. This selection includes news ' +
|
||||
'readers, podcast managers, torrent clients, chat bots, HTML browsers ' +
|
||||
'and plugins which support syndication feeds.';
|
||||
elementDiv.appendChild(elementH3);
|
||||
const brands = Object.keys(selection);
|
||||
let elementDivSel = document.createElement('div');
|
||||
elementDivSel.id = 'selection';
|
||||
for (let i = 0; i < brands.length; i++) {
|
||||
let brand = brands[i];
|
||||
elementSpan = document.createElement('span');
|
||||
let elementA = document.createElement('a');
|
||||
elementA.href = selection[brand].url;
|
||||
elementA.textContent = selection[brand].name;
|
||||
let elementImg = document.createElement('img');
|
||||
elementImg.src = 'graphic/' + selection[brand].image;
|
||||
elementSpan.appendChild(elementImg);
|
||||
elementSpan.appendChild(elementA);
|
||||
elementDivSel.appendChild(elementSpan);
|
||||
elementDiv.appendChild(elementDivSel);
|
||||
}
|
||||
let elementDivReturn = document.createElement('div');
|
||||
elementDivReturn.id = 'return';
|
||||
elementDivReturn.textContent = 'Return To PubSub...';
|
||||
elementDivReturn.addEventListener ('click', function() {
|
||||
document.querySelector('#selection-page').remove();
|
||||
});
|
||||
elementDiv.appendChild(elementDivReturn);
|
||||
document.body.appendChild(elementDiv);
|
||||
});
|
||||
}
|