├── .prettierrc ├── .gitignore ├── wrangler.toml.example ├── webpack.config.js ├── package.json ├── .github └── workflows │ └── codeql-analysis.yml ├── README.md ├── templates ├── headlines.handlebars ├── default.handlebars ├── headlines.precompiled.js └── default.precompiled.js ├── CODE_OF_CONDUCT.md ├── index.js └── LICENSE /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /dist 3 | **/*.rs.bk 4 | Cargo.lock 5 | bin/ 6 | pkg/ 7 | wasm-pack.log 8 | worker/ 9 | node_modules/ 10 | .cargo-ok 11 | wrangler.toml 12 | .wrangler 13 | -------------------------------------------------------------------------------- /wrangler.toml.example: -------------------------------------------------------------------------------- 1 | name = "worker-planet" 2 | main = "./worker/script.js" 3 | compatibility_date = "2023-05-18" 4 | node_compat = true 5 | account_id = "" 6 | workers_dev = true 7 | 8 | kv_namespaces = [ 9 | { binding = "WORKER_PLANET_STORE", id = "", preview_id = "" } 10 | ] 11 | 12 | [vars] 13 | FEEDS = "" 14 | MAX_SIZE = 50 15 | TITLE = "ADD_YOUR_TITLE_HERE" 16 | DESCRIPTION = "ADD_YOUR_DESCRIPTION_HERE" 17 | CUSTOM_URL = "URL_TO_BE_DISPLAYED_ON_RSS_FEED" 18 | CACHE_MAX_AGE = "NUMBER_OF_SECONDS" 19 | 20 | [triggers] 21 | crons = ["0 */2 * * *"] 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { 3 | WranglerJsCompatWebpackPlugin, 4 | } = require('wranglerjs-compat-webpack-plugin') 5 | 6 | module.exports = { 7 | mode: 'production', 8 | entry: './index.js', 9 | plugins: [new WranglerJsCompatWebpackPlugin()], 10 | resolve: { 11 | fallback: { 12 | stream: require.resolve('stream-browserify'), 13 | http: require.resolve('stream-http'), 14 | url: require.resolve('url/'), 15 | http: require.resolve('stream-http'), 16 | https: require.resolve('https-browserify'), 17 | timers: require.resolve('timers-browserify'), 18 | buffer: require.resolve('buffer/'), 19 | }, 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "worker-planet", 4 | "version": "1.2.0", 5 | "description": "Generate a single feed of content from multiple RSS/Atom sources. Runs on Cloudflare Workers.", 6 | "main": "./index.js", 7 | "scripts": { 8 | "format": "prettier --write '**/*.{js,css,json,md}' '!**/worker/*' '!**/templates/*'", 9 | "template": "handlebars -c handlebars/runtime", 10 | "build": "webpack", 11 | "dev": "webpack && wrangler dev", 12 | "deploy": "wrangler deploy" 13 | }, 14 | "author": "Gonçalo Valério ", 15 | "license": "AGPL-3.0", 16 | "devDependencies": { 17 | "buffer": "^6.0.3", 18 | "https-browserify": "^1.0.0", 19 | "prettier": "^1.18.2", 20 | "stream-browserify": "^3.0.0", 21 | "stream-http": "^3.2.0", 22 | "timers-browserify": "^2.0.12", 23 | "url": "^0.11.3", 24 | "webpack": "^5.89.0", 25 | "webpack-cli": "^4.10.0", 26 | "wrangler": "^3.19.0", 27 | "wranglerjs-compat-webpack-plugin": "^0.0.6" 28 | }, 29 | "dependencies": { 30 | "feed": "^4.2.2", 31 | "handlebars": "^4.7.7", 32 | "rss-parser": "^3.13.0", 33 | "striptags": "3.2.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '40 16 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Worker-planet 2 | 3 | This project is meant to be an easy way to aggregate, on a single page, content from multiple sources. 4 | 5 | It takes a list of existing RSS feeds, aggregates their contents and displays them in chronological order. 6 | 7 | It was inspired by other software packages, such as [Planet]() and [MoonMoon](https://moonmoon.org/), but runs on top of Cloudflare Workers instead of being deployed on your own server. 8 | 9 | This ends up being especially useful for communities to follow the work being done and published by their members. 10 | 11 | Below are some examples of existing "planets" that use different software: 12 | 13 | - https://planet.debian.org/ 14 | - https://planet.kde.org/ 15 | - https://planet.gnome.org/ 16 | 17 | An example of a live planet using `worker-planet` software is: 18 | 19 | - https://infosecplanet.ovalerio.net 20 | 21 | _If you wish to add your deployment to this list, feel free to create a PR._ 22 | 23 | # How to deploy 24 | 25 | 1. Clone this repository. 26 | 2. Install the project dependencies: `npm install`. 27 | 3. Create a `wrangler.toml` file based on the existing `wrangler.toml.example`. 28 | 4. Create KV namespaces and add their IDs to the `kv_namespaces` setting on `wrangler.toml`. 29 | 5. Add your `account_id` and customize all the `vars` in `wrangler.toml`. `FEEDS` should be a string of comma-separated URLs, one for each of the RSS/ATOM sources that will be part of your planet. 30 | 6. Build the worker bundle: `npm run build`. 31 | 7. Deploy your new worker: `npm run deploy`. 32 | 33 | **Note:** For the last step, you might need to set the `CLOUDFLARE_API_TOKEN` environment variable. 34 | 35 | ## Configuration variables 36 | 37 | - **FEEDS** - list of sources used to fetch the planet's content (separate each URL with a comma) 38 | - **TITLE** - Name of your planet (included in the generated HTML page and RSS feed) 39 | - **DESCRIPTION** - Free text to be included on the page (currently not used on the included template) 40 | - **MAX_SIZE** - Number of posts/entries that will be included on the page/feed 41 | - **CACHE_MAX_AGE** - To avoid hitting the KV store each time the content is fetched, the static content is cached. You should adjust this value to the frequency you pick for your cron. Defined in seconds (default: 3600) 42 | 43 | ## Customize the generated HTML 44 | 45 | Each community has its own identity, so you should be able to easily customize the look and feel 46 | of the generated page. To do so, before publishing, you can edit one of the existing templates in the `templates` folder. 47 | 48 | After that, you should "precompile" that file using the following command: 49 | 50 | > \$ npm run template -- templates/default.handlebars -f templates/default.precompiled.js 51 | 52 | If the template name you are using is different from `default`, you should change the following 2 lines in `index.js`: 53 | 54 | ```javascript 55 | import template from './templates/default.precompiled' 56 | ``` 57 | 58 | ```javascript 59 | let template = Handlebars.templates['default'] 60 | ``` 61 | -------------------------------------------------------------------------------- /templates/headlines.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{page_title}} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 |
27 |
28 |
29 |
30 |

{{page_title}}

31 |

{{page_description}}

32 |
33 |
34 |
35 | {{#each items}} 36 | {{#isRowElemN @index 4 0 }} 37 |
38 | {{/isRowElemN}} 39 |
40 |
41 |

{{title}}

42 | {{formattedDate}} 43 |
44 |

45 | {{{description}}} 46 |

47 |

48 | More 49 |

50 |
51 | {{#isRowElemN @index 4 3}} 52 |
53 | {{/isRowElemN}} 54 | {{/each}} 55 |
56 |
57 |
58 |

59 | All rights belong to the original authors. Powered by worker-planet. 60 |

61 |
62 |
63 | 64 |
65 |
66 | 67 | Sources 68 |
69 |

The content of this page is fetched from the following sources:

70 |

71 |

    72 | {{#each sources}} 73 |
  • {{name}}
  • 74 | {{/each}} 75 |
76 |

77 |
78 |
79 | 80 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ag_dubs@cloudflare.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /templates/default.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{page_title}} 9 | 10 | 11 | 12 | 13 | 14 | 16 | 18 | 33 | 34 | 35 | 36 | 58 | 59 |
60 | {{#each items}} 61 |
62 |
63 |
64 |
65 |

{{title}}

66 |

67 | Date: {{pubDate}}
68 | Source: {{source_title}} 69 |

70 | 71 | {{{content}}} 72 |
73 |
74 |
75 |
76 | {{/each}} 77 |
78 | 86 |
87 |
88 |

About

89 | 90 |
91 |
92 |

{{page_description}}

93 | 94 |
Sources
95 |
    96 | {{#each sources}} 97 |
  • {{name}}
  • 98 | {{/each}} 99 |
100 |
101 |
102 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Parser from 'rss-parser' 2 | import { Feed } from 'feed' 3 | import Handlebars from 'handlebars/runtime' 4 | import template from './templates/default.precompiled' 5 | import * as striptags from 'striptags' 6 | 7 | /** 8 | * Extra Handlerbars template helpers 9 | */ 10 | Handlebars.registerHelper('isRowElemN', function(index, rowItems, n, options) { 11 | return index % rowItems == n ? options.fn(this) : options.inverse(this) 12 | }) 13 | 14 | /** 15 | * Handle CRON jobs 16 | * Where information is gathered and HTML and RSS is generated. 17 | */ 18 | addEventListener('scheduled', event => { 19 | event.waitUntil(handleScheduled()) 20 | }) 21 | 22 | /** 23 | * Serve the existing generated elements. 24 | * CACHE is used to speed up the operation. 25 | */ 26 | addEventListener('fetch', event => { 27 | event.respondWith(handleRequest(event.request)) 28 | }) 29 | 30 | /** 31 | * Deliver aggregated content according to the formats requested 32 | * @param {Request} request 33 | * @returns Response 34 | */ 35 | async function handleRequest(request) { 36 | const cacheUrl = new URL(request.url) 37 | const cacheKey = new Request(cacheUrl.toString(), request) 38 | const cache = caches.default 39 | const cacheMaxAge = CACHE_MAX_AGE || 3600 40 | let response = await cache.match(cacheKey) 41 | if (response) return response 42 | 43 | const path = new URL(request.url).pathname 44 | 45 | if (path === '/') { 46 | let content = await WORKER_PLANET_STORE.get('html') 47 | response = new Response(content, { 48 | headers: { 49 | 'content-type': 'text/html;charset=UTF-8', 50 | 'Cache-Control': `max-age=${cacheMaxAge}`, 51 | }, 52 | }) 53 | } else if (path === '/rss') { 54 | let content = await WORKER_PLANET_STORE.get('rss') 55 | response = new Response(content, { 56 | headers: { 57 | 'content-type': 'application/rss+xml', 58 | 'Cache-Control': `max-age=${cacheMaxAge}`, 59 | }, 60 | }) 61 | } else if (path === '/atom') { 62 | let content = await WORKER_PLANET_STORE.get('atom') 63 | response = new Response(content, { 64 | headers: { 65 | 'content-type': 'application/atom+xml', 66 | 'Cache-Control': `max-age=${cacheMaxAge}`, 67 | }, 68 | }) 69 | } else { 70 | return new Response('', { status: 404 }) 71 | } 72 | await cache.put(cacheKey, response.clone()) 73 | return response 74 | } 75 | 76 | /** 77 | * Fetch all source feeds and generate the aggregated content 78 | */ 79 | async function handleScheduled() { 80 | let feeds = FEEDS.split(',') 81 | let content = [] 82 | let sources = [] 83 | 84 | let promises = [] 85 | for (let url of feeds) { 86 | promises.push(fetchAndHydrate(url)) 87 | } 88 | const results = await Promise.allSettled(promises) 89 | 90 | for (let [index, result] of results.entries()) { 91 | if (result.status == 'fulfilled') { 92 | let posts = result.value 93 | let title = posts[0].source_title 94 | let link = posts[0].source_link 95 | let name = title != '' ? title : new URL(link).host 96 | sources.push({ name, link }) 97 | content.push(...posts) 98 | } else { 99 | console.log(`Failed to fetch ${feeds[index]}`) 100 | console.log(result.reason) 101 | } 102 | } 103 | 104 | //sort all the elements chronologically (recent first) 105 | content.sort((a, b) => { 106 | let aDate = new Date(a.isoDate) 107 | let bDate = new Date(b.isoDate) 108 | if (aDate < bDate) { 109 | return 1 110 | } else if (aDate === bDate) { 111 | return 0 112 | } else { 113 | return -1 114 | } 115 | }) 116 | 117 | if (content.length > MAX_SIZE) { 118 | content = content.slice(0, MAX_SIZE) 119 | } 120 | 121 | // Generate feed 122 | let feed = createFeed(content) 123 | let html = createHTML(content, sources) 124 | // Store 125 | await WORKER_PLANET_STORE.put('rss', feed.rss2()) 126 | await WORKER_PLANET_STORE.put('atom', feed.atom1()) 127 | await WORKER_PLANET_STORE.put('html', html) 128 | } 129 | 130 | /** 131 | * Take a feed URL, fetch all items and attach source information 132 | * @param {String} feed The URL of the feed to be fetched and parsed 133 | * @returns Array containing all the feed items parsed by rss-parser 134 | */ 135 | async function fetchAndHydrate(feed) { 136 | console.log(`[fetchAndHydrate] start to fetch feed: ${feed}`) 137 | let resp = await fetch(feed) 138 | console.log(`[fetchAndHydrate] response: ${resp.status}`) 139 | let parser = new Parser() 140 | let content = await resp.text() 141 | let contentFeed = await parser.parseString(content) 142 | 143 | for (let item of contentFeed.items) { 144 | item.source_title = contentFeed.title 145 | item.source_link = contentFeed.link 146 | if ('content:encoded' in item) { 147 | item.content = item['content:encoded'] 148 | } 149 | } 150 | console.log( 151 | `[fetchAndHydrate] Finished fetch feed: ${feed}. ${contentFeed.items.length} items gathered`, 152 | ) 153 | return contentFeed.items 154 | } 155 | 156 | /** 157 | * Builds a feed object from the provided items 158 | * @param {Array} items parsed by rss-parser 159 | * @return Feed object created by feed 160 | */ 161 | function createFeed(items) { 162 | console.log(`[createFeed] start building the aggregated feed`) 163 | const feed = new Feed({ 164 | title: TITLE, 165 | description: DESCRIPTION, 166 | id: CUSTOM_URL, 167 | link: CUSTOM_URL, 168 | }) 169 | 170 | for (let item of items) { 171 | feed.addItem({ 172 | title: item.title, 173 | id: item.guid, 174 | link: item.link, 175 | description: item.contentSnippet, 176 | content: item.content, 177 | author: [ 178 | { 179 | name: item.creator, 180 | email: '', 181 | link: item.source_link, 182 | }, 183 | ], 184 | contributor: [], 185 | date: new Date(item.isoDate), 186 | }) 187 | } 188 | console.log(`[createFeed] Finished building the aggregated feed`) 189 | return feed 190 | } 191 | /** 192 | * Generate the HTML page with the aggregated contents 193 | * @param {Array} items parsed by rss-parser 194 | * @returns String with HTML page containing the parsed contents 195 | */ 196 | function createHTML(items, sources) { 197 | console.log(`[createHTML] building the HTML document`) 198 | let template = Handlebars.templates['default'] 199 | let dateFormatter = new Intl.DateTimeFormat('pt-PT', { timeZone: 'UTC' }) 200 | 201 | for (let item of items) { 202 | let shortdescription = striptags(item.content).substring(0, 250) 203 | item.description = shortdescription ? shortdescription + ' [...]' : '' 204 | item.formattedDate = item.pubDate 205 | ? dateFormatter.format(new Date(item.pubDate)) 206 | : '' 207 | } 208 | 209 | return template({ 210 | items: items, 211 | sources: sources, 212 | page_title: TITLE, 213 | page_description: DESCRIPTION, 214 | }) 215 | } 216 | -------------------------------------------------------------------------------- /templates/headlines.precompiled.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require("handlebars/runtime"); var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 2 | templates['headlines'] = template({"1":function(container,depth0,helpers,partials,data) { 3 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) { 4 | if (Object.prototype.hasOwnProperty.call(parent, propertyName)) { 5 | return parent[propertyName]; 6 | } 7 | return undefined 8 | }; 9 | 10 | return ((stack1 = (lookupProperty(helpers,"isRowElemN")||(depth0 && lookupProperty(depth0,"isRowElemN"))||alias2).call(alias1,(data && lookupProperty(data,"index")),4,0,{"name":"isRowElemN","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":36,"column":8},"end":{"line":38,"column":23}}})) != null ? stack1 : "") 11 | + "
\n
\n

" 12 | + alias4(((helper = (helper = lookupProperty(helpers,"title") || (depth0 != null ? lookupProperty(depth0,"title") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data,"loc":{"start":{"line":41,"column":18},"end":{"line":41,"column":27}}}) : helper))) 13 | + "

\n " 14 | + alias4(((helper = (helper = lookupProperty(helpers,"formattedDate") || (depth0 != null ? lookupProperty(depth0,"formattedDate") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"formattedDate","hash":{},"data":data,"loc":{"start":{"line":42,"column":21},"end":{"line":42,"column":38}}}) : helper))) 15 | + "\n
\n

\n " 16 | + ((stack1 = ((helper = (helper = lookupProperty(helpers,"description") || (depth0 != null ? lookupProperty(depth0,"description") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"description","hash":{},"data":data,"loc":{"start":{"line":45,"column":14},"end":{"line":45,"column":31}}}) : helper))) != null ? stack1 : "") 17 | + "\n

\n

\n More\n

\n
\n" 20 | + ((stack1 = (lookupProperty(helpers,"isRowElemN")||(depth0 && lookupProperty(depth0,"isRowElemN"))||alias2).call(alias1,(data && lookupProperty(data,"index")),4,3,{"name":"isRowElemN","hash":{},"fn":container.program(4, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":51,"column":8},"end":{"line":53,"column":23}}})) != null ? stack1 : ""); 21 | },"2":function(container,depth0,helpers,partials,data) { 22 | return "
\n"; 23 | },"4":function(container,depth0,helpers,partials,data) { 24 | return "
\n"; 25 | },"6":function(container,depth0,helpers,partials,data) { 26 | var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) { 27 | if (Object.prototype.hasOwnProperty.call(parent, propertyName)) { 28 | return parent[propertyName]; 29 | } 30 | return undefined 31 | }; 32 | 33 | return "
  • " 36 | + alias4(((helper = (helper = lookupProperty(helpers,"name") || (depth0 != null ? lookupProperty(depth0,"name") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":73,"column":37},"end":{"line":73,"column":45}}}) : helper))) 37 | + "
  • \n"; 38 | },"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) { 39 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) { 40 | if (Object.prototype.hasOwnProperty.call(parent, propertyName)) { 41 | return parent[propertyName]; 42 | } 43 | return undefined 44 | }; 45 | 46 | return "\n\n \n \n \n \n " 47 | + alias4(((helper = (helper = lookupProperty(helpers,"page_title") || (depth0 != null ? lookupProperty(depth0,"page_title") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"page_title","hash":{},"data":data,"loc":{"start":{"line":7,"column":11},"end":{"line":7,"column":25}}}) : helper))) 48 | + "\n \n \n Sources\n \n \n
    \n
    \n
    \n
    \n

    " 53 | + alias4(((helper = (helper = lookupProperty(helpers,"page_title") || (depth0 != null ? lookupProperty(depth0,"page_title") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"page_title","hash":{},"data":data,"loc":{"start":{"line":30,"column":16},"end":{"line":30,"column":30}}}) : helper))) 54 | + "

    \n

    " 55 | + alias4(((helper = (helper = lookupProperty(helpers,"page_description") || (depth0 != null ? lookupProperty(depth0,"page_description") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"page_description","hash":{},"data":data,"loc":{"start":{"line":31,"column":16},"end":{"line":31,"column":36}}}) : helper))) 56 | + "

    \n
    \n
    \n
    \n" 57 | + ((stack1 = lookupProperty(helpers,"each").call(alias1,(depth0 != null ? lookupProperty(depth0,"items") : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":35,"column":6},"end":{"line":54,"column":15}}})) != null ? stack1 : "") 58 | + "
    \n
    \n
    \n

    \n All rights belong to the original authors. Powered by worker-planet.\n

    \n
    \n
    \n \n
    \n
    \n \n Sources\n
    \n

    The content of this page is fetched from the following sources:

    \n

    \n

      \n" 59 | + ((stack1 = lookupProperty(helpers,"each").call(alias1,(depth0 != null ? lookupProperty(depth0,"sources") : depth0),{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":72,"column":12},"end":{"line":74,"column":21}}})) != null ? stack1 : "") 60 | + "
    \n

    \n
    \n
    \n \n"; 61 | },"useData":true}); 62 | -------------------------------------------------------------------------------- /templates/default.precompiled.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require("handlebars/runtime"); var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 2 | templates['default'] = template({"1":function(container,depth0,helpers,partials,data) { 3 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) { 4 | if (Object.prototype.hasOwnProperty.call(parent, propertyName)) { 5 | return parent[propertyName]; 6 | } 7 | return undefined 8 | }; 9 | 10 | return "
    \n
    \n
    \n
    \n

    " 13 | + alias4(((helper = (helper = lookupProperty(helpers,"title") || (depth0 != null ? lookupProperty(depth0,"title") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data,"loc":{"start":{"line":65,"column":54},"end":{"line":65,"column":63}}}) : helper))) 14 | + "

    \n

    \n Date: " 15 | + alias4(((helper = (helper = lookupProperty(helpers,"pubDate") || (depth0 != null ? lookupProperty(depth0,"pubDate") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"pubDate","hash":{},"data":data,"loc":{"start":{"line":67,"column":27},"end":{"line":67,"column":38}}}) : helper))) 16 | + "
    \n Source: " 19 | + alias4(((helper = (helper = lookupProperty(helpers,"source_title") || (depth0 != null ? lookupProperty(depth0,"source_title") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"source_title","hash":{},"data":data,"loc":{"start":{"line":68,"column":55},"end":{"line":68,"column":71}}}) : helper))) 20 | + "\n

    \n\n " 21 | + ((stack1 = ((helper = (helper = lookupProperty(helpers,"content") || (depth0 != null ? lookupProperty(depth0,"content") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"content","hash":{},"data":data,"loc":{"start":{"line":71,"column":12},"end":{"line":71,"column":25}}}) : helper))) != null ? stack1 : "") 22 | + "\n
    \n
    \n
    \n
    \n"; 23 | },"3":function(container,depth0,helpers,partials,data) { 24 | var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) { 25 | if (Object.prototype.hasOwnProperty.call(parent, propertyName)) { 26 | return parent[propertyName]; 27 | } 28 | return undefined 29 | }; 30 | 31 | return "
  • " 34 | + alias4(((helper = (helper = lookupProperty(helpers,"name") || (depth0 != null ? lookupProperty(depth0,"name") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":97,"column":33},"end":{"line":97,"column":41}}}) : helper))) 35 | + "
  • \n"; 36 | },"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) { 37 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) { 38 | if (Object.prototype.hasOwnProperty.call(parent, propertyName)) { 39 | return parent[propertyName]; 40 | } 41 | return undefined 42 | }; 43 | 44 | return "\n\n\n\n \n \n \n " 45 | + alias4(((helper = (helper = lookupProperty(helpers,"page_title") || (depth0 != null ? lookupProperty(depth0,"page_title") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"page_title","hash":{},"data":data,"loc":{"start":{"line":8,"column":9},"end":{"line":8,"column":23}}}) : helper))) 46 | + "\n \n \n 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------