├── debian ├── source │ └── format ├── nim-package-directory.install ├── nim-package-directory.conf ├── rules ├── nim-package-directory.postinst ├── control ├── changelog └── nim-package-directory.service ├── public ├── js │ ├── jquery.min.js │ └── highlight.min.js ├── css │ ├── an-old-hope.css │ ├── an-old-hope.min.css │ ├── style.css │ ├── lime.min.css │ └── nimdoc.out.css ├── img │ ├── nimble.png │ ├── loading1.gif │ ├── loading2.gif │ ├── logo-simple.png │ ├── nimble-logo.png │ ├── feed-icon-14x14.png │ ├── nimble_search_logo.png │ ├── badge-loading.svg │ └── logo.svg └── search.xml ├── .gitignore ├── conf.json.example ├── nim.cfg ├── templates ├── build_output.tmpl ├── doc_files_list.tmpl ├── doc_success.svg ├── success.svg ├── doc_running.svg ├── doc_waiting.svg ├── version-template-blue.svg ├── build_running.svg ├── doc_fail.svg ├── fail.svg ├── build_waiting.svg ├── loader.tmpl ├── rss.tmpl ├── jsondoc_pkg_symbols.tmpl ├── jsondoc_symbols.tmpl ├── about.tmpl ├── build_history.tmpl ├── pkg_list.tmpl ├── pkg.tmpl ├── base.tmpl └── home.tmpl ├── .editorconfig ├── package_directory.nimble ├── email.nim ├── persist.nim ├── .circleci └── config.yml ├── README.adoc ├── tests ├── test_package_directory.nim ├── test_signatures_functional.nim ├── test_signatures.nim └── test_functional.nim ├── util.nim ├── friendly_timeinterval.nim ├── github.nim ├── signatures.nim ├── package_directory.nim └── LICENSE /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /public/js/jquery.min.js: -------------------------------------------------------------------------------- 1 | /usr/share/javascript/jquery/jquery.min.js -------------------------------------------------------------------------------- /public/js/highlight.min.js: -------------------------------------------------------------------------------- 1 | /usr/share/javascript/highlight.js/highlight.min.js -------------------------------------------------------------------------------- /public/css/an-old-hope.css: -------------------------------------------------------------------------------- 1 | /usr/share/javascript/highlight.js/styles/an-old-hope.css -------------------------------------------------------------------------------- /public/img/nimble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FedericoCeratto/nim-package-directory/HEAD/public/img/nimble.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | makefile 3 | conf.json 4 | node_modules 5 | .cache.json 6 | package_directory 7 | pkgs.log 8 | -------------------------------------------------------------------------------- /public/img/loading1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FedericoCeratto/nim-package-directory/HEAD/public/img/loading1.gif -------------------------------------------------------------------------------- /public/img/loading2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FedericoCeratto/nim-package-directory/HEAD/public/img/loading2.gif -------------------------------------------------------------------------------- /public/img/logo-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FedericoCeratto/nim-package-directory/HEAD/public/img/logo-simple.png -------------------------------------------------------------------------------- /public/img/nimble-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FedericoCeratto/nim-package-directory/HEAD/public/img/nimble-logo.png -------------------------------------------------------------------------------- /public/img/feed-icon-14x14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FedericoCeratto/nim-package-directory/HEAD/public/img/feed-icon-14x14.png -------------------------------------------------------------------------------- /public/img/nimble_search_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FedericoCeratto/nim-package-directory/HEAD/public/img/nimble_search_logo.png -------------------------------------------------------------------------------- /debian/nim-package-directory.install: -------------------------------------------------------------------------------- 1 | package_directory usr/bin 2 | public var/lib/nim_package_directory 3 | debian/nim-package-directory.conf /etc 4 | -------------------------------------------------------------------------------- /debian/nim-package-directory.conf: -------------------------------------------------------------------------------- 1 | { 2 | "github_token": "CHANGEME", 3 | "packages_list_fname": "packages.json", 4 | "public_baseurl": "https://CHANGEME", 5 | "port": 5000, 6 | "tmp_nimble_root_dir": "/tmp/nim_package_directory/cache" 7 | } 8 | -------------------------------------------------------------------------------- /conf.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "github_token": "", 3 | "log_fname": "pkgs.log", 4 | "packages_list_fname": "packages.json", 5 | "public_baseurl": "https://nimble.directory", 6 | "port": 5000, 7 | "tmp_nimble_root_dir": "/var/lib/nim_package_directory/cache" 8 | } -------------------------------------------------------------------------------- /nim.cfg: -------------------------------------------------------------------------------- 1 | -d:ssl 2 | --checks:on 3 | --assertions:on 4 | --lineTrace:on 5 | hint[XDeclaredButNotUsed]=on 6 | gcc.options.always = "-w -D_FORTIFY_SOURCE=2 -O1 -Wformat -Wformat-security -fPIE -fstack-protector-all" 7 | gcc.options.linker = "-ldl -fPIE -pie -z relro -z now" 8 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | export DH_VERBOSE=1 3 | # hardened using nim.cfg 4 | 5 | %: 6 | dh $@ 7 | 8 | override_dh_auto_build: 9 | nimble --verbose c -d:systemd -d:release --checks:on --assertions:on --stackTrace:on --lineTrace:on package_directory.nim 10 | 11 | override_dh_auto_test: 12 | true 13 | -------------------------------------------------------------------------------- /templates/build_output.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc generate_run_output_page(pname, run_output: string, build_time, expire_time: Time): string = 3 | # result = "" 4 |
5 |

Build output from ${pname}

6 |

Built on ${build_time}

7 |
${run_output}
8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.scss] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [*.tmpl] 16 | indent_style = space 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /package_directory.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.1" 4 | author = "Federico Ceratto" 5 | description = "Nim package directory" 6 | license = "GPLv3" 7 | 8 | bin = @["package_directory"] 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.0.0", "jester >= 0.4.1", "tempfile", "sdnotify", "statsd_client > 0.1.0", "morelogging >= 0.2.0" 13 | 14 | task builddeb, "Generate deb": 15 | exec "dpkg-buildpackage -us -uc -b -j4" 16 | -------------------------------------------------------------------------------- /debian/nim-package-directory.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ "$1" = "configure" ]; then 6 | if ! getent passwd nim-package-directory >/dev/null; then 7 | adduser --quiet --system --group --no-create-home --home /var/lib/nim_package_directory nim-package-directory 8 | fi 9 | mkdir -p /var/lib/nim_package_directory/ 10 | chown nim-package-directory -Rv /var/lib/nim_package_directory/ 11 | fi 12 | 13 | #DEBHELPER# 14 | 15 | exit 0 16 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: nim-package-directory 2 | Section: web 3 | Priority: optional 4 | Maintainer: Federico Ceratto 5 | Build-Depends: debhelper-compat (= 13), libsystemd-dev 6 | Standards-Version: 4.6.0 7 | 8 | Package: nim-package-directory 9 | Architecture: any 10 | Depends: 11 | ${shlibs:Depends}, 12 | ${misc:Depends}, 13 | libjs-highlight.js, 14 | libjs-jquery, 15 | nim, 16 | unzip 17 | Description: Nim package directory 18 | Nim package directory service 19 | -------------------------------------------------------------------------------- /templates/doc_files_list.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc generate_doc_files_list_page(pkg_name: string, m: PkgDocMetadata): string = 3 | # result = "" 4 |
5 |
6 |

Hosted Documentation for ${pkg_name}

7 | 12 | ${m.fnames.len} documented files.
13 |
14 |
15 | -------------------------------------------------------------------------------- /public/css/an-old-hope.min.css: -------------------------------------------------------------------------------- 1 | .hljs-comment,.hljs-quote{color:#b6b18b}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#eb3c54}.hljs-built_in,.hljs-builtin-name,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#e7ce56}.hljs-attribute{color:#ee7c2b}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:#4fb4d7}.hljs-section,.hljs-title{color:#78bb65}.hljs-keyword,.hljs-selector-tag{color:#b45ea4}.hljs{display:block;overflow-x:auto;background:#1c1d21;color:#c0c5ce;padding:.5em}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} -------------------------------------------------------------------------------- /templates/doc_success.svg: -------------------------------------------------------------------------------- 1 | Doc buildDoc buildOKOK 2 | -------------------------------------------------------------------------------- /templates/success.svg: -------------------------------------------------------------------------------- 1 | Install testInstall testOKOK -------------------------------------------------------------------------------- /public/img/badge-loading.svg: -------------------------------------------------------------------------------- 1 | Install testWaitWaitOK 2 | -------------------------------------------------------------------------------- /templates/doc_running.svg: -------------------------------------------------------------------------------- 1 | Doc buildDoc build...... 2 | -------------------------------------------------------------------------------- /templates/doc_waiting.svg: -------------------------------------------------------------------------------- 1 | Doc buildDoc build...... 2 | -------------------------------------------------------------------------------- /templates/version-template-blue.svg: -------------------------------------------------------------------------------- 1 | versionversion$#$# 2 | -------------------------------------------------------------------------------- /templates/build_running.svg: -------------------------------------------------------------------------------- 1 | Install testInstall test...... 2 | -------------------------------------------------------------------------------- /templates/doc_fail.svg: -------------------------------------------------------------------------------- 1 | Doc buildDoc buildFailingFailing 2 | -------------------------------------------------------------------------------- /templates/fail.svg: -------------------------------------------------------------------------------- 1 | Install testInstall testFailFailing 2 | -------------------------------------------------------------------------------- /templates/build_waiting.svg: -------------------------------------------------------------------------------- 1 | Install testInstall testwaitwait ⏱ 2 | -------------------------------------------------------------------------------- /email.nim: -------------------------------------------------------------------------------- 1 | import std/[asyncdispatch, smtp, strutils] 2 | 3 | type 4 | Config* = object 5 | smtpAddress: string 6 | smtpPort: int 7 | smtpUser: string 8 | smtpPassword: string 9 | mlistAddress: string 10 | 11 | proc sendEMail(config: Config, subject, message, recipient: string, from_addr = "forum@nim-lang.org") {.async.} = 12 | var client = newAsyncSmtp() 13 | await client.connect(config.smtpAddress, Port(config.smtpPort)) 14 | if config.smtpUser.len > 0: 15 | await client.auth(config.smtpUser, config.smtpPassword) 16 | 17 | let toList = @[recipient] 18 | let encoded = createMessage(subject, message, 19 | toList, @[], []) 20 | 21 | await client.sendMail(from_addr, toList, 22 | $encoded) 23 | 24 | #let c = Config(smtpAddress: "localhost", smtpPort: 2525, smtpUser: "", smtpPassword: "") 25 | -------------------------------------------------------------------------------- /templates/loader.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc generate_loader_page(): string = 3 | # result = "" 4 | 5 | 6 |
7 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /templates/rss.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc generate_rss_feed(title="", desc="", url:Uri, buildDate="", pubDate="", ttl=3600, items:seq[RssItem]): string = 3 | # result = "" 4 | 5 | 6 | 7 | ${title} 8 | ${desc} 9 | ${url} 10 | 11 | ${buildDate} 12 | ${pubDate} 13 | ${ttl} 14 | 15 | # for item in items: 16 | 17 | ${item.title} 18 | ${item.desc} 19 | ${item.url} 20 | ${item.guid} 21 | ${item.pubDate} 22 | 23 | # end for 24 | 25 | 26 | -------------------------------------------------------------------------------- /persist.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Nimble package directory - persistent data 3 | # 4 | # Copyright 2016-2021 Federico Ceratto 5 | # Released under GPLv3 License, see LICENSE file 6 | # 7 | 8 | import std/[marshal, streams] 9 | from std/net import Port 10 | 11 | const 12 | pkgs_history_fname = "pkgs_history.json" 13 | conf_fname = "/etc/nim-package-directory.conf" 14 | 15 | proc save_pkgs_history*(ph: seq[string]) = 16 | store(newFileStream(pkgs_history_fname, fmWrite), ph) 17 | 18 | proc load_pkgs_history*(): seq[string] = 19 | try: 20 | load(newFileStream(pkgs_history_fname, fmRead), result) 21 | except: 22 | result = @[] 23 | save_pkgs_history(result) 24 | 25 | # conf 26 | 27 | type 28 | Conf* = object of RootObj 29 | github_token*, packages_list_fname*, public_baseurl*, tmp_nimble_root_dir*: string 30 | port*: Port 31 | 32 | proc load_conf*(): Conf = 33 | result = to[Conf](readFile(conf_fname)) 34 | -------------------------------------------------------------------------------- /templates/jsondoc_pkg_symbols.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc generate_jsondoc_pkg_symbols_page(symbols: PkgSymbols, url: string): string = 3 | # result = "" 4 | 5 |
6 |
7 |
${symbols.len} entries found
8 |
9 | 10 | #for pname, symbol in symbols: 11 |
12 |
13 |
14 |
${symbol.code}
15 |
${symbol.desc}
16 |
Type: ${symbol.itype}
17 | # if symbol.filepath.len > 0: 18 |
Filename: ${symbol.filepath[1..^1]}
19 | # end 20 | 22 | View source 23 | 24 |
25 |
26 |
27 | #end for 28 | -------------------------------------------------------------------------------- /public/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | machine: true 5 | steps: 6 | - run: echo 'export PATH=~/.nimble/bin:$PATH' >> $BASH_ENV 7 | - checkout 8 | # Reuse cached Nim compiler 9 | - restore_cache: 10 | key: compiler-0002 11 | - run: 12 | command: | 13 | if [ -f ~/.nimble/bin/choosenim ]; then 14 | echo "Updating Nim using choosenim" 15 | choosenim stable 16 | else 17 | echo "Installing choosenim and Nim" 18 | wget https://raw.githubusercontent.com/dom96/choosenim/master/scripts/choosenim-unix-init.sh 19 | sh choosenim-unix-init.sh -y 20 | fi 21 | - save_cache: 22 | key: compiler-0002 23 | paths: 24 | - ~/.nimble 25 | - ~/.choosenim 26 | - run: 27 | command: | 28 | nimble build -y 29 | #NIMPKGDIR_ENABLE_FUNCTEST=1 nim c -p=. -r tests/test_functional.nim 30 | - store_artifacts: 31 | path: test-reports/ 32 | destination: tr1 33 | - store_test_results: 34 | path: test-reports/ 35 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | 2 | === Nim package directory 3 | 4 | Currently running at https://nimble.directory 5 | 6 | .Features: 7 | - [x] Package search 8 | - [x] Display GitHub readme 9 | - [ ] Display GitLab readme 10 | - [ ] Display BitBucket readme 11 | - [x] Fetch & install packages, serve badges 12 | - [x] Build and serve pkg docs 13 | - [x] New packages RSS feed 14 | - [x] Search symbols from jsondoc 15 | - [x] Simple API 16 | - [x] Build history at /build_history.html 17 | - [x] Package count at /api/v1/package_count 18 | - [ ] Pkg metadata signing 19 | 20 | ======= 21 | 22 | .Prerequisites : 23 | - systemd watchdog 24 | - optional: Netdata or StatsD to receive application metrics 25 | 26 | .Deployment: 27 | 28 | sudo apt-get install nim dpkg-dev debhelper libsystemd-dev 29 | nimble builddeb 30 | # Locate and install the package 31 | sudo apt install ../nim-package-directory_0.1.5_amd64.deb 32 | sudo systemctl status nim-package-directory.service 33 | sudo journalctl -f --identifier=package_directory 34 | 35 | .Development: 36 | - For Development, edit /etc/nim-package-directory.conf 37 | - Execute `nim c -r package_directory.nim`. 38 | - Browse http://localhost:5000 39 | -------------------------------------------------------------------------------- /templates/jsondoc_symbols.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc generate_jsondoc_symbols_page(jd_symbols: PkgSymbols): string = 3 | # result = "" 4 | 5 |
6 |
7 |

${jd_symbols.len} entries found

8 |
9 | 10 | #for pname, symbol in jd_symbols: 11 |
12 |
13 |
14 |
${symbol.code}
15 |
Desc: ${symbol.desc}
16 |
Type: ${symbol.itype}
17 |
File: ${symbol.filepath}
18 |
Package: ${pname}
19 |
20 | 23 | 25 | View on GitHub 26 | 27 | 28 |
29 |
30 | #end for 31 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | nim-package-directory (0.2.0) unstable; urgency=medium 2 | 3 | * Restyle, cleanup 4 | 5 | -- Federico Ceratto Wed, 05 Jan 2022 15:04:33 +0000 6 | 7 | nim-package-directory (0.1.7) unstable; urgency=medium 8 | 9 | * Fix docgen, improve styling 10 | 11 | -- Federico Ceratto Wed, 05 Jan 2022 15:04:33 +0000 12 | 13 | nim-package-directory (0.1.6) unstable; urgency=medium 14 | 15 | * Better stats, minor fixes 16 | 17 | -- Federico Ceratto Mon, 27 Dec 2021 16:31:02 +0000 18 | 19 | nim-package-directory (0.1.5) unstable; urgency=medium 20 | 21 | * Add build queue 22 | 23 | -- Federico Ceratto Sat, 06 Jun 2020 18:55:33 +0100 24 | 25 | nim-package-directory (0.1.4) unstable; urgency=medium 26 | 27 | * Update ping 28 | 29 | -- Federico Ceratto Wed, 13 May 2020 00:51:58 +0100 30 | 31 | nim-package-directory (0.1.3) unstable; urgency=medium 32 | 33 | * Fix RSS 34 | 35 | -- Federico Ceratto Thu, 26 Mar 2020 12:01:34 +0000 36 | 37 | nim-package-directory (0.1.1) unstable; urgency=medium 38 | 39 | * Initial release 40 | 41 | -- Federico Ceratto Sun, 08 Oct 2017 13:10:54 +0100 42 | -------------------------------------------------------------------------------- /debian/nim-package-directory.service: -------------------------------------------------------------------------------- 1 | # nim-package-directory systemd target 2 | 3 | [Unit] 4 | Description=nim-package-directory 5 | Documentation=man:nim-package-directory 6 | Documentation=https://github.com/FedericoCeratto/nim-package-directory 7 | After=network.target 8 | Wants=network-online.target 9 | ConditionPathExists=/etc/nim-package-directory.conf 10 | 11 | [Service] 12 | Type=simple 13 | ExecStart=/usr/bin/package_directory 14 | TimeoutStopSec=10 15 | KillMode=mixed 16 | KillSignal=SIGTERM 17 | 18 | User=nim-package-directory 19 | Restart=always 20 | RestartSec=2s 21 | LimitNOFILE=65536 22 | 23 | WorkingDirectory=/var/lib/nim_package_directory 24 | WatchdogSec=60 25 | 26 | # Hardening 27 | NoNewPrivileges=yes 28 | CapabilityBoundingSet= 29 | SystemCallFilter=~@cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @clock @debug @keyring @mount @privileged @reboot @setuid @swap @memlock 30 | SystemCallErrorNumber=EPERM 31 | # ipc, signal are needed 32 | 33 | ProtectSystem=strict 34 | PrivateDevices=yes 35 | PrivateUsers=yes 36 | PrivateTmp=yes 37 | ProtectHome=yes 38 | ProtectKernelModules=true 39 | ProtectKernelTunables=yes 40 | 41 | StandardOutput=syslog+console 42 | StandardError=syslog+console 43 | 44 | ReadWriteDirectories=-/proc/self 45 | ReadWriteDirectories=-/var/run 46 | ReadWriteDirectories=-/var/lib/nim_package_directory 47 | 48 | 49 | [Install] 50 | WantedBy=multi-user.target 51 | -------------------------------------------------------------------------------- /templates/about.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc generate_about_page(): string = 3 | # result = "" 4 |
5 | 6 |

Welcome to the Nim Package Directory

7 |

This service allows you to explore Nim packages known to Nimble.
8 | It tests package installation and generates documentation using "nim doc".
9 | It also generates badges with the output of install test and documentation generation that you can link to.

10 | 11 |

The badges look like:

12 |

To link install test and documentation generation in MarkDown use:

13 |

14 |
[![Build Status](https://nimble.directory/ci/badges/jester/nimdevel/status.svg)](https://nimble.directory/ci/badges/jester/nimdevel/output.html)
15 |
[![Build Status](https://nimble.directory/ci/badges/jester/nimdevel/docstatus.svg)](https://nimble.directory/ci/badges/jester/nimdevel/doc_build_output.html)
16 |

Learn here how you can add your own packages.

17 |

You can contribute to this website on GitHub.

18 |
19 | -------------------------------------------------------------------------------- /public/search.xml: -------------------------------------------------------------------------------- 1 | 2 | Nimble 3 | Search the Nim package directory 4 | UTF-8 5 | data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAAUAAAAF////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAAIAAABbAAAAlQAAAKIAAACbAAAAmwAAAKIAAACVAAAAWwAAAAL///8A////AP///wD///8A////AAAAABQAAADAAAAAYwAAAA3///8A////AP///wD///8AAAAADQAAAGMAAADAAAAAFP///wD///8A////AP///wAAAACdAAAAOv///wD///8A////AP///wD///8A////AP///wD///8AAAAAOgAAAJ3///8A////AP///wAAAAAnAAAAcP///wAAAAAoAAAASv///wD///8A////AP///wAAAABKAAAAKP///wAAAABwAAAAJ////wD///8AAAAAgQAAABwAAACIAAAAkAAAAJMAAACtAAAAFQAAABUAAACtAAAAkwAAAJAAAACIAAAAHAAAAIH///8A////AAAAAKQAAACrAAAAaP///wD///8AAAAARQAAANIAAADSAAAARf///wD///8AAAAAaAAAAKsAAACk////AAAAADMAAACcAAAAnQAAABj///8A////AP///wAAAAAYAAAAGP///wD///8A////AAAAABgAAACdAAAAnAAAADMAAAB1AAAAwwAAAP8AAADpAAAAsQAAAE4AAAAb////AP///wAAAAAbAAAATgAAALEAAADpAAAA/wAAAMMAAAB1AAAAtwAAAOkAAAD/AAAA/wAAAP8AAADvAAAA3gAAAN4AAADeAAAA3gAAAO8AAAD/AAAA/wAAAP8AAADpAAAAtwAAAGUAAAA/AAAA3wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADfAAAAPwAAAGX///8A////AAAAAEgAAADtAAAAvwAAAL0AAADGAAAA7wAAAO8AAADGAAAAvQAAAL8AAADtAAAASP///wD///8A////AP///wD///8AAAAAO////wD///8A////AAAAAIcAAACH////AP///wD///8AAAAAO////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A//8AAP//AAD4HwAA7/cAAN/7AAD//wAAoYUAAJ55AACf+QAAh+EAAAAAAADAAwAA4AcAAP5/AAD//wAA//8AAA== 6 | https://nimble.directory/img/nimble_search_logo.png 7 | 8 | https://nimble.directory 9 | 10 | -------------------------------------------------------------------------------- /tests/test_package_directory.nim: -------------------------------------------------------------------------------- 1 | 2 | import unittest, 3 | sequtils, 4 | strutils, 5 | tables 6 | 7 | import package_directory 8 | 9 | const nimble_install_output = """ 10 |  Info Hint: used config file '/etc/nim.cfg' [Conf] 11 |  Info Hint: used config file '/home/fede/.config/nim.cfg' [Conf] 12 |  Warning: File inside package 'nim_package_directory' is outside of permitted namespace, should be named 'nim_package_directory.nim' but was named 'micron.nim' instead. This will be an error in the future. 13 |  Hint: Rename this file to 'nim_package_directory.nim', move it into a 'nim_package_directory/' subdirectory, or prevent its installation by adding `skipFiles = @["micron.nim"]` to the .nimble file. See https://github.com/nim-lang/nimble#libraries for more info. 14 | Downloading https://github.com/dom96/jester using git 15 |  Warning: File 'example2.nim' inside package 'jester' is outside of the permitted namespace, should be inside a directory named 'jester' but is in a directory named 'tests' instead. This will be an error in the future. 16 |  Hint: Rename the directory to 'jester' or prevent its installation by adding `skipDirs = @["tests"]` to the .nimble file. 17 |  Verifying dependencies for jester@0.1.1 18 |  Warning: No nimblemeta.json file found in /home/fede/.nimble/pkgs/nake-1.8 19 |  Installing jester@0.1.1 20 |  Prompt: jester-0.1.1 already exists. Overwrite? [y/N] 21 |  Answer:  22 | """ 23 | 24 | suite "test pkgs": 25 | #test "search": 26 | # load_packages() 27 | # let ct = search_packages("high level game, little math") 28 | # assert ct.len == 31 29 | # assert ct.largest[0] == "linagl", $ct.largest 30 | 31 | test "translate_term_colors": 32 | let o = translate_term_colors nimble_install_output 33 | assert o.contains("m[") == false 34 | assert o.contains("[0m") == false 35 | 36 | test "CountTable sorted": 37 | var a = initCountTable[string]() 38 | a.inc("B", 2) 39 | a.inc("A", 3) 40 | a.inc("C", 1) 41 | let b = sorted(a) 42 | assert b.smallest() == ("C", 1) 43 | assert toSeq(b.keys()) == @["A", "B", "C"] 44 | assert toSeq(b.values()) == @[3, 2, 1] 45 | 46 | test "remove HTML": 47 | const 48 | html = "Sends status and Content-Type: text/html." 49 | exp = "Sends status and Content-Type: text/html." 50 | check exp == strip_html(html) 51 | -------------------------------------------------------------------------------- /templates/build_history.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc generate_build_history_page(build_history: Deque[BuildHistoryItem], pkgs_waiting_build: HashSet[string], pkgs_building: HashSet[string]): string = 3 | # const build_success_badge = slurp "success.svg" 4 | # const build_fail_badge = slurp "fail.svg" 5 | # const doc_success_badge = slurp "doc_success.svg" 6 | # const doc_fail_badge = slurp "doc_fail.svg" 7 | # result = "" 8 |
9 |
10 |

Pending:

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | # for pname in pkgs_waiting_build: 19 | 20 | # end 21 | 22 |
name
${pname}
23 | 24 |

Running:

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | # for pname in pkgs_building: 33 | 34 | # end 35 | 36 |
name
${pname}
37 | 38 |

History:

39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | # for i in build_history: 50 | 51 | 54 | 55 | 65 | 82 | 83 | # end 84 | 85 |
namebuild datebuild outputdoc gen output
52 | ${i.name} 53 | ${i.build_time} 56 | # if i.build_status == BuildStatus.OK: 57 | 58 | ${build_success_badge} 59 | # else: 60 | 61 | ${build_fail_badge} 62 | 63 | # end 64 | 66 | # if i.doc_build_status == BuildStatus.OK: 67 | 68 | ${doc_success_badge} 69 | 70 | # else: 71 | # if i.build_status == BuildStatus.OK: 72 | 73 | ${doc_fail_badge} 74 | 75 | # else: 76 | 77 | ${doc_fail_badge} 78 | 79 | # end 80 | # end 81 |
86 |
87 |
88 | -------------------------------------------------------------------------------- /tests/test_signatures_functional.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Nimble package directory - functional tests 3 | # 4 | # Copyright 2016 Federico Ceratto 5 | # Released under GPLv3 License, see LICENSE file 6 | # 7 | # WARNING: do not run functional tests on a live instance! 8 | 9 | import httpclient 10 | import json 11 | import os 12 | import osproc 13 | import strutils, unittest 14 | 15 | import signatures 16 | 17 | from micron import sign_and_publish 18 | 19 | # The package directory is running here 20 | const pkgdir_url = "http://localhost:5000" 21 | 22 | let test_pkg_chunk = %* { 23 | "name": "pkg_dir_testpkg", 24 | "tags": [ 25 | "test", 26 | "library", 27 | ], 28 | "method": "git", 29 | "license": "MIT", 30 | "web": "https://github.com/FedericoCeratto/nim-package-directory", 31 | "url": "https://github.com/FedericoCeratto/nim-package-directory", 32 | "description": "Test package", 33 | "downloads": """https://github.com/DonnchaC/onionbalance/releases .*/onionbalance-([\d\.]+).tar.gz""", 34 | } 35 | 36 | let 37 | # Temporary test keys. Generating temporary keys on each run is too time-consuming. 38 | # Generate them externally with: 39 | # gpg --batch -quick-random --passphrase '' --quick-gen-key 'Nim Test' 40 | key1 = getEnv("K1") 41 | key2 = getEnv("K2") 42 | key3 = getEnv("K3") 43 | 44 | doAssert key1.len == 18 45 | doAssert key2.len == 18 46 | doAssert key3.len == 18 47 | 48 | suite "Micron functional test": 49 | 50 | test "end to end test": 51 | ## Test Micron and the Package Directory 52 | 53 | const 54 | test_dir = "/tmp/pacdir_test" 55 | 56 | # create test pgk, publish it on directory 57 | var metadata = test_pkg_chunk.copy() 58 | metadata["authorized_keys"] = newJArray() 59 | metadata["authorized_keys"].add newJString key1 60 | metadata["authorized_keys"].add newJString key2 61 | echo "Publishing package metadata..." 62 | sign_and_publish(pkgdir_url, metadata, key1) 63 | 64 | # update packages, scan for package, 65 | # fetch pkg and check validity 66 | let output = execProcess "./micron list-downloads $# pkg_dir_testpkg" % pkgdir_url 67 | echo output 68 | 69 | # Update nimble pkg metadata, push the update to directory 70 | # update packages, scan for package, 71 | # fetch pkg and check validity 72 | 73 | # Update nimble pkg metadata, push the update to directory 74 | # using wrong key, check for failure 75 | 76 | # Hijack pkg on directory, nimble-update packages and check for warning 77 | 78 | # Fetch binary release, check buildbot signatures 79 | 80 | 81 | 82 | 83 | 84 | 85 | # 86 | # 87 | 88 | 89 | 90 | 91 | discard """ 92 | nimble_bin = expandTilde "~/.nimble/bin/nimble" 93 | 94 | # Configure Nimble to fetch from localhost:5000 95 | putEnv("HOME", test_dir) 96 | createDir(test_dir / ".config/nimble") 97 | writeFile(test_dir / ".config/nimble/nimble.ini", "" 98 | [PackageList] 99 | name = "Official" 100 | url = "http://localhost:5000/packages.json" 101 | "") 102 | 103 | assert existsFile nimble_bin 104 | echo execProcess(nimble_bin & " update") 105 | """ 106 | -------------------------------------------------------------------------------- /templates/pkg_list.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc generate_pkg_list_page(pkgs: seq[Pkg]): string = 3 | # result = "" 4 |
5 |
6 |
7 |

${pkgs.len} Package(s) Found

8 |
9 |
10 |
11 | # for pkg in pkgs: 12 |
13 |
14 |

${pkg["name"].str}

15 |

${pkg["description"].str}

16 |
    17 |
  • 18 | 19 | # let url = pkg["url"].str.strip(false, true, {'/'}).rsplit('/', maxsplit=2) 20 | # if url.len == 3: 21 | ${url[1]} 22 | # else: 23 | Unknown 24 | # end 25 |
  • 26 |
  • 27 | 28 | Repository 29 |
  • 30 |
  • 31 | 32 | ${pkg["license"].str} 33 |
  • 34 |
35 |
36 |
37 | # end 38 |
39 |
40 | -------------------------------------------------------------------------------- /util.nim: -------------------------------------------------------------------------------- 1 | import std/[algorithm, json, strutils, sequtils] 2 | import jester, morelogging 3 | 4 | when defined(systemd): 5 | let log* = newJournaldLogger() 6 | else: 7 | let log* = newStdoutLogger() 8 | 9 | proc log_debug*(args: varargs[string, `$`]) = 10 | log.debug(args.join(" ")) 11 | 12 | proc log_info*(args: varargs[string, `$`]) = 13 | log.info(args.join(" ")) 14 | 15 | proc log_req*(request: Request) = 16 | ## Log request data 17 | var path = "" 18 | for c in request.path: 19 | if len(path) > 300: 20 | path.add "..." 21 | break 22 | let o = c.ord 23 | if o < 32 or o > 126: 24 | path.add o.toHex() 25 | else: 26 | path.add c 27 | log_info "serving $# $# $#" % [request.ip, $request.reqMeth, path] 28 | 29 | proc is_newer*(b, a: string): int = 30 | ## Based on Nimble implementation, compares versions a.b.c by simply 31 | ## comparing the integers :-/ 32 | for (ai, bi) in zip(a.split('.'), b.split('.')): 33 | let aa = parseInt(ai) 34 | let bb = parseInt(bi) 35 | if bb > aa: 36 | return 1 37 | elif aa > bb: 38 | return -1 39 | 40 | return -1 41 | 42 | proc extract_latest_version*(releases: JsonNode): (string, JsonNode) = 43 | ## Extracts the release metadata chunk from `releases` matching the latest release 44 | var latest_version = "-1.-1.-1" 45 | for r in releases: 46 | let version = r["tag_name"].str.strip().strip(trailing = false, chars = {'v'}) 47 | if is_newer(version, latest_version) > 0: 48 | latest_version = version 49 | result = (version, r) 50 | log_debug "Picking latest version from GH tags: ", latest_version 51 | 52 | proc extract_latest_versions_str*(releases: JsonNode): JsonNode = 53 | ## Extracts latest releases as JSON array 54 | result = newJArray() 55 | var vers: seq[string] = @[] 56 | for r in releases: 57 | let version = r["tag_name"].str.strip().strip(trailing = false, chars = {'v'}) 58 | vers.add version 59 | let x = min(vers.len, 3) 60 | for v in vers.sorted(is_newer)[^x..^1]: 61 | result.add newJString(v) 62 | 63 | proc uniescape*(inp: string): string = 64 | for c in inp: 65 | let o = c.ord 66 | if o < 32 or o > 126: 67 | let q = "\\u00" & o.toHex()[^2..^1] 68 | result.add q 69 | else: 70 | result.add c 71 | 72 | # proc `+`(t1, t2: Time): Time {.borrow.} 73 | 74 | proc strip_html*(html: string): string = 75 | # Assumes that any < > that is not part of HTML tags has been escaped 76 | # Everything that matches <.*> is removed, including invalid tags 77 | result = newStringOfCap(html.len) 78 | var inside_tag = false 79 | for c in html: 80 | if inside_tag == false: 81 | if c == '<': 82 | inside_tag = true 83 | else: 84 | result.add c 85 | elif c == '>': 86 | inside_tag = false 87 | 88 | proc cleanup_whitespace*(s: string): string = 89 | ## Removes trailing whitespace and normalizes line endings to LF. 90 | result = newStringOfCap(s.len) 91 | var i = 0 92 | while i < s.len: 93 | if s[i] == ' ': 94 | var j = i+1 95 | while s[j] == ' ': inc j 96 | if s[j] == '\c': 97 | inc j 98 | if s[j] == '\L': inc j 99 | result.add '\L' 100 | i = j 101 | elif s[j] == '\L': 102 | result.add '\L' 103 | i = j+1 104 | else: 105 | result.add ' ' 106 | inc i 107 | elif s[i] == '\c': 108 | inc i 109 | if s[i] == '\L': inc i 110 | result.add '\L' 111 | elif s[i] == '\L': 112 | result.add '\L' 113 | inc i 114 | else: 115 | result.add s[i] 116 | inc i 117 | if result[^1] != '\L': 118 | result.add '\L' 119 | -------------------------------------------------------------------------------- /friendly_timeinterval.nim: -------------------------------------------------------------------------------- 1 | 2 | import std/[times, strutils] 3 | 4 | proc toFriendlyInterval*(i: TimeInterval, approx = 10): string = 5 | ## Convert TimeInterval to human-friendly description 6 | ## e.g. "5 minutes ago" 7 | result = "" 8 | var approx = approx 9 | let vals = [i.years, i.months, i.days, i.hours, i.minutes, i.seconds] 10 | var i = i 11 | var negative = false 12 | for v in vals: 13 | if v < 0: 14 | negative = true 15 | continue 16 | 17 | for i, fname in ["year", "month", "day", "hour", "minute", "second"]: 18 | var fvalue = vals[i] 19 | if approx != 0 and fvalue != 0: 20 | if result.len != 0: 21 | result.add ", " 22 | 23 | if negative: 24 | fvalue = -fvalue 25 | 26 | result.add "$# $#" % [$fvalue, fname] 27 | if fvalue != 1: 28 | result.add "s" 29 | approx.dec 30 | 31 | if result.len == 0: 32 | result.add "now" 33 | elif negative: 34 | result.add " from now" 35 | else: 36 | result.add " ago" 37 | 38 | 39 | proc toNonLinearInterval(a, b: Time): TimeInterval = 40 | ## 41 | const 42 | minutes_s = 60 43 | hours_s = minutes_s * 60 44 | days_s = hours_s * 24 45 | months_s = 2592000 46 | years_s = months_s * 12 47 | 48 | var seconds = (b - a).seconds.int 49 | let years = seconds div years_s 50 | seconds -= years * years_s 51 | let months = seconds div months_s 52 | seconds -= months * months_s 53 | let days = seconds div days_s 54 | seconds -= days * days_s 55 | let hours = seconds div hours_s 56 | seconds -= hours * hours_s 57 | let minutes = seconds div minutes_s 58 | seconds -= minutes * minutes_s 59 | result = initInterval(seconds, minutes, hours, days, 60 | months, years) 61 | 62 | proc toNonLinearInterval2(a, b: Time): TimeInterval = 63 | var remaining = (b - a).seconds.int 64 | let years = remaining div 31536000 65 | remaining -= years * 31536000 66 | let months = remaining div 2592000 67 | remaining -= months * 2592000 68 | let days = remaining div 86400 69 | remaining -= days * 86400 70 | let hours = remaining div 3600 71 | remaining -= hours * 3600 72 | let minutes = remaining div 60 73 | remaining -= minutes * 60 74 | result = initInterval(remaining.int, minutes.int, hours.int, days.int, 75 | months.int, years.int) 76 | 77 | proc toNonLinearInterval3(a, b: Time): (TimeInterval, bool) = 78 | ## 79 | var i = b.toTimeInterval - a.toTimeInterval 80 | 81 | let in_future = ((b - a).seconds.int < 0) 82 | if in_future: 83 | i.seconds *= -1 84 | i.minutes *= -1 85 | i.hours *= -1 86 | i.days *= -1 87 | i.months *= -1 88 | i.years *= -1 89 | 90 | if i.seconds < 0: 91 | i.seconds.inc 60 92 | i.minutes.dec 93 | if i.minutes < 0: 94 | i.minutes.inc 60 95 | i.hours.dec 96 | if i.hours < 0: 97 | i.hours.inc 24 98 | i.days.dec 99 | if i.days < 0: 100 | i.days += 30 101 | i.months.dec 102 | if i.months < 0: 103 | i.months.inc 12 104 | i.years.dec 105 | 106 | return (i, in_future) 107 | 108 | proc toFriendlyInterval3*(i: TimeInterval, in_future: bool, approx = 10): string = 109 | ## Convert TimeInterval to human-friendly description 110 | ## e.g. "5 minutes ago" 111 | result = "" 112 | var approx = approx 113 | let vals = [i.years, i.months, i.days, i.hours, i.minutes, i.seconds] 114 | var i = i 115 | 116 | for i, fname in ["year", "month", "day", "hour", "minute", "second"]: 117 | var fvalue = vals[i] 118 | if approx != 0 and fvalue != 0: 119 | if result.len != 0: 120 | result.add ", " 121 | 122 | result.add "$# $#" % [$fvalue, fname] 123 | if fvalue != 1: 124 | result.add "s" 125 | approx.dec 126 | 127 | if result.len == 0: 128 | result.add "now" 129 | elif in_future: 130 | result.add " from now" 131 | else: 132 | result.add " ago" 133 | 134 | 135 | 136 | proc toFriendlyInterval*(a, b: Time, approx = 10): string = 137 | # creates inc 138 | #let i = b.toTimeInterval - a.toTimeInterval 139 | 140 | #let i2 = initInterval(seconds=int(b - a)) 141 | let (i, in_future) = toNonLinearInterval3(a, b) 142 | for v in [i.years, i.months, i.days, i.hours, i.minutes, i.seconds]: 143 | assert v >= 0 144 | toFriendlyInterval3(i, in_future, approx) 145 | 146 | #proc toFriendlyInterval(a, b: TimeInfo, approx = 10): string = 147 | # (a - b).fromSeconds.toTimeInterval.toFriendlyInterval(approx) 148 | 149 | -------------------------------------------------------------------------------- /templates/pkg.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc generate_pkg_page(pkg: Pkg): string = 3 | # result = "" 4 | 5 | # let package_name = pkg["name"].str 6 | 7 |
8 |
9 |

${package_name}

10 |

11 | #for tag in pkg["tags"]: 12 | 13 | 16 | 17 | #end for 18 |

19 |

${pkg["description"].str}

20 | 21 | 22 | 23 | 24 |
25 | Need help? Read Nimble 26 |
27 | 28 |
29 |
30 | #if pkg.has_key("github_readme"): 31 | ${pkg["github_readme"].str} 32 | #else: 33 |

The package README is not present or from an unsupported forge.

34 | #end 35 |
36 |
37 |
38 | #if pkg.has_key("github_owner"): 39 |

Author: 40 | ${pkg["github_owner"].str} 41 |

42 | #end 43 | 44 | #if pkg.has_key("github_latest_versions_str"): 45 | #if pkg["github_latest_versions_str"].getElems().len > 0: 46 |

Latest versions: 47 | #for semver in pkg["github_latest_versions_str"].getElems(): 48 | ${semver.str} 49 | #end 50 |

51 | #else: 52 |

No tagged versions available

53 | #end 54 | #end 55 |

56 | Licence: 57 | #let licns = pkg["license"].str.toLowerAscii.strip 58 | #if licns == "mit": 59 | MIT 60 | #elif licns == "apache2" or licns == "apache": 61 | Apache 2 62 | #elif licns == "bsd": 63 | BSD 64 | #elif licns == "bsd2" or licns == "bsd 2-clause": 65 | BSD 2-Clause 66 | #elif licns == "bsd3" or licns == "bsd 3-clause": 67 | BSD 3-Clause 68 | #elif licns == "gplv2": 69 | GPL2 70 | #elif licns == "gplv3": 71 | GPL3 72 | #elif licns == "gpl": 73 | GPL 74 | #elif licns == "lgplv2": 75 | LGPL2 76 | #elif licns == "lgplv3": 77 | LGPL3 78 | #elif licns == "lgpl": 79 | LGPL 80 | #elif licns == "cc0": 81 | Creative Commons Zero 82 | #elif licns == "cc" or licns == "cc-by-nc-sa" or licns == "cc-by-nc-nd": 83 | Creative Commons 84 | #elif licns == "wtfpl": 85 | WTFPL 86 | #else: 87 | ${pkg["license"].str} 88 | #end 89 |

90 | 91 | #if pkg.has_key("web"): 92 |

Project website

93 | #end 94 | #if pkg.has_key("doc"): 95 |

Docs

96 | #end if 97 |
98 |
99 |
100 |
101 | 108 | -------------------------------------------------------------------------------- /tests/test_signatures.nim: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import os 4 | import unittest 5 | import signatures 6 | 7 | import sequtils 8 | 9 | let 10 | # Temporary test keys 11 | # gpg --batch -quick-random --passphrase '' --quick-gen-key 'Nim Test' 12 | key1 = getEnv("K1") 13 | key2 = getEnv("K2") 14 | key3 = getEnv("K3") 15 | 16 | doAssert key1.len == 18 17 | doAssert key2.len == 18 18 | doAssert key3.len == 18 19 | 20 | let j = %* { 21 | "books": @["Robot Dreams"], 22 | "name": "Isaac", 23 | "authorized_keys": @["1234"], 24 | "description": "blah", 25 | "license": "GPLv3", 26 | "method": "git", 27 | "name": "testfoo", 28 | "tags": @["foo", "bar"], 29 | "url": "https://uu", 30 | "web": "https://abc", 31 | } 32 | 33 | let sig = generate_gpg_signature(j, key1) 34 | #let sig2 = generate_gpg_signature(j, key2) 35 | 36 | suite "gpg": 37 | 38 | test "verify": 39 | echo verify_gpg_signature(j, sig) 40 | 41 | test "verify allowed": 42 | discard verify_gpg_signature_is_allowed(j, sig, @[key1]) 43 | 44 | test "verify not allowed": 45 | expect Exception: 46 | discard verify_gpg_signature_is_allowed(j, sig, @["0x6F31BC44F51QQQQQ"]) 47 | 48 | test "verify fails on incorrect signature": 49 | #FIXME 50 | if false: 51 | var j2 = %* {"name": "Foo", "books": ["Robot Dreams"]} 52 | echo verify_gpg_signature(j2, sig) 53 | expect Exception: 54 | echo verify_gpg_signature(j2, sig) 55 | 56 | test "embed_gpg_signature twice": 57 | let n = copy(j) 58 | n.embed_gpg_signature(key1) 59 | assert n["signatures"].len == 1 60 | n.embed_gpg_signature(key2) 61 | assert n["signatures"].len == 2 62 | 63 | test "verify_enough_allowed_gpg_signatures": 64 | let n = copy(j) 65 | n.embed_gpg_signature(key1) 66 | n.verify_enough_allowed_gpg_signatures(@[key1], 1) 67 | expect Exception: 68 | n.verify_enough_allowed_gpg_signatures(@[key1], 2) 69 | n.verify_enough_allowed_gpg_signatures(@[key2], 1) 70 | n.embed_gpg_signature(key2) 71 | n.verify_enough_allowed_gpg_signatures(@[key1], 1) 72 | n.verify_enough_allowed_gpg_signatures(@[key2], 1) 73 | n.verify_enough_allowed_gpg_signatures(@[key1, key2], 2) 74 | expect Exception: 75 | n.verify_enough_allowed_gpg_signatures(@[key1, key2], 3) 76 | 77 | suite "dload": 78 | # test "retr": 79 | # let url = "https://raw.githubusercontent.com/nim-lang/packages/master/.travis.yml" 80 | # 81 | # download_file(url, "/tmp/pj", check_modified_time=false) 82 | # download_file(url, "/tmp/pj") 83 | 84 | test "full dev package install": 85 | if false: 86 | let rfn = "/tmp/roster.json" 87 | echo download_file( 88 | "https://raw.githubusercontent.com/nim-lang/packages/master/.travis.yml" 89 | , rfn) 90 | let roster = load_and_verify_roster("roster.json") # local copy 91 | 92 | echo download_file( 93 | "https://raw.githubusercontent.com/nim-lang/packages/master/packages.json", 94 | "/tmp/packages.json") 95 | 96 | test "the whole story": 97 | let 98 | core_key = key1 99 | trusted_key = key2 100 | owner_key = key3 101 | # Create a roster, signed by a core dev 102 | block create_roster: 103 | let roster = %* { 104 | "signatures": newJArray(), 105 | "trusted_keys": [trusted_key.newJString, ] 106 | } 107 | roster.embed_gpg_signature(core_key) 108 | writeFile("/tmp/sigtest/remote_roster.json", $roster) 109 | 110 | block create_packages_json: 111 | let pkgs = parseJson(""" 112 | [ 113 | { 114 | "name": "argument_parser", 115 | "url": "https://github.com/Xe/argument_parser/", 116 | "method": "git", 117 | "tags": [ 118 | "library", 119 | "commandline", 120 | "arguments", 121 | "switches", 122 | "parsing" 123 | ], 124 | "description": "Provides a complex commandline parser", 125 | "license": "MIT", 126 | "web": "https://github.com/Xe/argument_parser" 127 | }, 128 | ] """) 129 | # sign the first entry using the owner key 130 | pkgs[0]["owner_keys"] = newJArray() 131 | pkgs[0]["owner_keys"].add owner_key.newJString 132 | pkgs[0].embed_gpg_signature(owner_key) 133 | # sign the whole file 134 | #pkgs.embed_gpg_signature(trusted_key) 135 | writeFile("/tmp/sigtest/packages.json", $pkgs) 136 | 137 | # a local HTTP server should serve the roster 138 | # Download the roster 139 | block download_and_verify_roster: 140 | assert download_file("http://127.0.0.1:8000/remote_roster.json", 141 | "/tmp/sigtest/roster.json") 142 | let roster = load_and_verify_roster( 143 | fname="/tmp/sigtest/roster.json", 144 | accepted_keys = @[core_key], 145 | required_sigs_num=1 146 | ) 147 | 148 | block download_packages_json: 149 | assert download_file("http://127.0.0.1:8000/packages.json", 150 | "/tmp/sigtest/packages.json") 151 | 152 | block verify_package_metadata: 153 | let pkgs = "/tmp/sigtest/packages.json".readFile.parseJson() 154 | let pkg = pkgs[0] 155 | #FIXME pkg.verify_package_metadata() 156 | 157 | #TODO git fetch and check git tag signature 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /tests/test_functional.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Nimble package directory - functional test 3 | # 4 | # Copyright 2016-2022 Federico Ceratto 5 | # Released under GPLv3 License, see LICENSE file 6 | # 7 | # WARNING: do not run functional tests on a live instance! 8 | 9 | # Functional-test every aspect of the package directory 10 | # except signing 11 | 12 | import httpclient 13 | import json 14 | import strutils, unittest 15 | 16 | from os import existsEnv, sleep 17 | 18 | if not existsEnv("NIMPKGDIR_ENABLE_FUNCTEST"): 19 | echo "Set NIMPKGDIR_ENABLE_FUNCTEST to enable functional tests" 20 | quit(1) 21 | 22 | const url="http://localhost:5000" 23 | 24 | proc get(url: string): string = 25 | echo " fetching $#" % url 26 | return newHttpClient().getContent(url) 27 | 28 | proc post(url: string): string = 29 | echo " post to $#" % url 30 | return newHttpClient().postContent(url) 31 | 32 | 33 | 34 | suite "functional tests": 35 | 36 | test "index": 37 | var page = get url 38 | check page.contains "Recently" 39 | 40 | test "search / show pkg list": 41 | # users search pkg 42 | var page = get(url & "/search?query=framework") 43 | check page.contains "Chromium Embedded Framework" 44 | page = get(url & "/search?query=framework") 45 | check page.contains "Chromium Embedded Framework" 46 | 47 | test "build jester pkg": 48 | 49 | test "JSON status: unknown": 50 | check "unknown" in get(url & "/api/v1/status/jester") 51 | 52 | # users look at pkg metadata 53 | # look at pkg github readme 54 | var page = get(url & "/pkg/jester") 55 | check page.contains "Jester provides a DSL" 56 | page = get(url & "/pkg/jester") 57 | check page.contains "Jester provides a DSL" 58 | # Check string from the GH readme 59 | check page.contains "Routes will be executed in the order" 60 | check page.contains "0.5.0" 61 | 62 | for cnt in 1..100: 63 | if "done" in newHttpClient().getContent(url & "/api/v1/status/jester"): 64 | break 65 | sleep 250 66 | if cnt == 100: quit(1) 67 | 68 | test "JSON status: done": 69 | let status = get(url & "/api/v1/status/jester").parseJSON() 70 | check status["status"].getStr() == "done" 71 | check status["build_time"].getStr().startsWith("202") 72 | 73 | test "fetch packages.json": 74 | var page = get url & "/packages.json" 75 | let pkgs_1 = page.parseJson() 76 | check pkgs_1.len > 100 77 | 78 | test "show hosted doc file list": 79 | var page = get url & "/docs/jester" 80 | check page.contains "jester.html" 81 | page = get url & "/docs/jester" 82 | check page.contains "jester.html" 83 | 84 | test "show hosted doc file": 85 | var page = get url & "/docs/jester/jester.html" 86 | # From jester's docgen 87 | check page.contains "IP address of the requesting client" 88 | page = get url & "/docs/jester/jester.html" 89 | check page.contains "IP address of the requesting client" 90 | 91 | test "new packages RSS": 92 | var page = get url & "/packages.xml" 93 | check page.contains """""" 94 | check page.contains """""" 95 | 96 | test "jester status.svg": 97 | var page = get url & "/ci/badges/jester/nimdevel/status.svg" 98 | check page.contains ">OK<" 99 | 100 | test "jester version.svg": 101 | var page = get url & "/ci/badges/jester/version.svg" 102 | check page.contains "version" 103 | 104 | test "jsondoc": 105 | var page = get url & "/searchitem?query=newSettings" 106 | check page.contains "324" 107 | check page.contains "getCurrentDir" 108 | check page.contains "1 entries found" 109 | check page.contains "jester.nim" 110 | 111 | test "global symbol search - empty": 112 | var page = get url & "/searchitem?query=nothingToBeFoundHere" 113 | check page.contains "0 entries found" 114 | 115 | test "global symbol search - normalizeUri": 116 | # assumes jester has been built 117 | var page = get url & "/searchitem?query=normalizeUri" 118 | check page.contains "1 entries found" 119 | # TODO: fix page style and content 120 | 121 | test "global symbol search - API": 122 | var page = get url & "/api/v1/search_symbol?symbol=sendHeaders" 123 | check page.startsWith("[") 124 | check page.endswith("]") 125 | 126 | test "package symbol search - normalizeUri": 127 | # assumes jester has been built 128 | var page = post url & "/searchitem_pkg?pkg_name=jester&query=normalizeUri" 129 | check page.contains "1 entries found" 130 | 131 | test "package symbol search - sendHeaders": 132 | # assumes jester has been built 133 | var page = post url & "/searchitem_pkg?pkg_name=jester&query=sendHeaders" 134 | check page.contains "3 entries found" 135 | check page.contains "Filename: jester.nim" 136 | check page.contains "Type: skProc" 137 | check page.contains "https://github.com/dom96/jester/blob/master/jester.nim#L95" 138 | check page.contains "https://github.com/dom96/jester/blob/master/jester.nim#L108" 139 | check page.contains "https://github.com/dom96/jester/blob/master/jester.nim#L113" 140 | 141 | #TODO: API 142 | 143 | # test "/ci/install_report": 144 | # discard 145 | 146 | # test "/ci/badges/@pkg_name/version.svg": 147 | # ## Version badge 148 | # discard 149 | 150 | 151 | 152 | 153 | 154 | #look generated doc page 155 | #nimble-install pkg 156 | # verify pkg_owner signature on pkg metadata 157 | # verify pkg_ower signature on repo git tag 158 | # 159 | #new and updated pkgs RSS feed 160 | #hotlinking into badges 161 | # 162 | # pkg_owner upload new pkg metadata 163 | # pkg owner update pkg metadata 164 | # pkg owner look at install output 165 | # pkg owner look at nim doc output 166 | -------------------------------------------------------------------------------- /github.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Nim package directory 3 | # GitHub interface 4 | # 5 | 6 | import std/[algorithm, asyncdispatch, httpclient, json, strutils, tables, times] 7 | import jester, statsd_client 8 | import util, persist 9 | 10 | type 11 | Pkg* = JsonNode 12 | Pkgs* = TableRef[string, Pkg] 13 | 14 | const 15 | github_caching_time* = 600 16 | 17 | let conf* = load_conf() 18 | let stats* = newStatdClient(prefix = "nim_package_directory") 19 | let github_token_headers = newHttpHeaders({ 20 | "Authorization": "token $#" % conf.github_token}) 21 | 22 | # volatile caches 23 | var volatile_cache_github_trending_last_update_time = 0 24 | var volatile_cache_github_trending: seq[JsonNode] = @[] 25 | 26 | proc fetch_from_github(url: string): Future[string] {.async.} = 27 | ## Fetch content from GitHub asychronously 28 | log_debug "fetching ", url 29 | try: 30 | let ac = newAsyncHttpClient() 31 | ac.headers = github_token_headers 32 | return await ac.getContent(url) 33 | except: 34 | log_debug "failed to fetch content ", url 35 | log_debug getCurrentExceptionMsg() 36 | return "" 37 | 38 | proc fetch_json*(url: string): Future[JsonNode] {.async.} = 39 | ## Fetch JSON from GitHub asynchronously 40 | let response = await fetch_from_github(url) 41 | return response.parseJson() 42 | 43 | proc fetch_nimble_packages*(): Future[string] {.async.} = 44 | ## Fetch the packages.json file from GitHub 45 | let url = "https://raw.githubusercontent.com/nim-lang/packages/master/packages.json" 46 | return await fetch_from_github(url) 47 | 48 | # TODO: return strings, not newJStrings 49 | 50 | proc fetch_github_readme*(owner, repo_name: string): Future[JsonNode] {.async.} = 51 | ## Fetch README.* from GitHub 52 | let url = "https://api.github.com/repos/$#/$#/readme" % [owner, repo_name] 53 | log_debug "fetching ", url 54 | try: 55 | let ac = newAsyncHttpClient() 56 | ac.headers = github_token_headers 57 | ac.headers["Accept"] = "application/vnd.github.v3.html" # necessary 58 | let readme = await ac.getContent(url) 59 | return newJString readme 60 | except: 61 | log_debug "failed to fetch content ", url 62 | log_debug getCurrentExceptionMsg() 63 | return newJString "" 64 | 65 | proc fetch_github_doc_pages*(owner, repo_name: string): Future[JsonNode] {.async.} = 66 | ## Fetch documentation pages from GitHub 67 | let url = "https://$#.github.io/$#/index.html" % [owner.toLowerAscii, repo_name] 68 | log_debug "checking ", url 69 | let resp = await newAsyncHttpClient().get(url) 70 | if resp.status.startswith("200"): 71 | return newJString url 72 | else: 73 | log_debug "doc not found at ", url 74 | return newJString "" 75 | 76 | proc fetch_github_versions*(pkg: Pkg, owner, repo_name: string) {.async.} = 77 | ## Fetch versions from GH from releases and tags 78 | ## Set github_versions, github_latest_version, github_latest_version_url 79 | let github_tags_url = "https://api.github.com/repos/$#/$#/tags" % [owner, repo_name] 80 | log_debug "fetching GitHub tags ", github_tags_url 81 | var version_names = newJArray() 82 | try: 83 | let ac = newAsyncHttpClient() 84 | ac.headers = github_token_headers 85 | let rtags = await ac.getContent(github_tags_url) 86 | let tags = parseJson(rtags) 87 | for t in tags: 88 | let name = t["name"].str.strip(trailing = false, chars = {'v'}) 89 | if name.len > 0: 90 | version_names.add newJString name 91 | except: 92 | log_info getCurrentExceptionMsg() 93 | pkg["github_versions"] = version_names 94 | pkg["github_latest_version"] = newJString "none" 95 | pkg["github_latest_version_url"] = newJString "" 96 | return 97 | 98 | pkg["github_versions"] = version_names 99 | log_debug "fetched $# GH versions" % $len(version_names) 100 | 101 | let github_api_releases_url = "https://api.github.com/repos/$#/$#/releases" % [owner, repo_name] 102 | log_debug "fetching GH releases ", github_api_releases_url 103 | var releases: JsonNode 104 | try: 105 | releases = await fetch_json(github_api_releases_url) 106 | except: 107 | log_debug getCurrentExceptionMsg() 108 | releases = newJArray() 109 | 110 | if releases.len > 0: 111 | let (latest_version, meta) = extract_latest_version(releases) 112 | doAssert meta != nil 113 | pkg["github_latest_version"] = newJString latest_version 114 | pkg["github_latest_versions_str"] = extract_latest_versions_str(releases) 115 | pkg["github_latest_version_url"] = newJString meta["tarball_url"].str 116 | pkg["github_latest_version_time"] = newJString meta["published_at"].str 117 | 118 | else: 119 | log_debug getCurrentExceptionMsg() 120 | log_debug "No releases - falling back to tags" 121 | var latest = "0" 122 | for v in version_names: 123 | if v.str > latest: 124 | latest = v.str 125 | if latest == "0": 126 | pkg["github_latest_version"] = newJString "none" 127 | pkg["github_latest_version_url"] = newJString "" 128 | else: 129 | pkg["github_latest_version"] = newJString latest.strip 130 | pkg["github_latest_version_url"] = newJString( 131 | "https://github.com/$#/$#/archive/v$#.tar.gz" % [owner, repo_name, latest] 132 | ) 133 | 134 | pkg["github_latest_version_time"] = newJString "" 135 | 136 | proc fetch_trending_packages*(request: Request, pkgs: Pkgs): Future[seq[Pkg]] {.async.} = 137 | ## Fetches trending repositories written in Nim from GitHub, and filters packages.json down to those 138 | if volatile_cache_github_trending_last_update_time + 139 | github_caching_time > epochTime().int: 140 | return volatile_cache_github_trending 141 | 142 | let date = utc(getTime() - 14.days).format("yyyy-MM-dd") 143 | let url = "https://api.github.com/search/repositories?q=language:nim+pushed:>$#&per_page=$#sort=$#&page=$#" % [date, "20", "updated", "1"] 144 | log_info "searching GH repos: '$#'" % url 145 | let query_res = await fetch_json(url) 146 | 147 | let github_trending_pkgs: seq[JsonNode] = 148 | query_res["items"].elems 149 | .sortedByIt(it["updated_at"].str).reversed() 150 | 151 | # Filter the package list to trending packages + add GitHub info 152 | var trending_pkgs: seq[Pkg] = @[] 153 | for p in github_trending_pkgs: 154 | # Many packages prefix their GitHub name with `nim-`: remove these 155 | # (there may be false positives, but considerably fewer than otherwise) 156 | let package_name: string = 157 | if p["name"].str.len > 4 and p["name"].str[0..3] == "nim-": 158 | p["name"].str[4..^1].normalize() 159 | else: 160 | p["name"].str.normalize() 161 | 162 | # FIXME: checking names is not completely reliable, check urls instead 163 | if pkgs.hasKey(package_name): 164 | var current_pkg = pkgs[package_name] 165 | # Add GitHub stargazer count 166 | current_pkg.add("stargazers_count", p["stargazers_count"]) 167 | # Add last updated time (pushed_at is more accurate than updated_at) 168 | current_pkg.add("pushed_at", p["pushed_at"]) 169 | # Add GitHub author 170 | current_pkg.add("owner", p["owner"]) 171 | 172 | trending_pkgs.add(current_pkg) 173 | else: 174 | log_debug "package " & package_name & " not found" 175 | 176 | trending_pkgs = trending_pkgs.sortedByIt(it["pushed_at"].str).reversed() 177 | 178 | volatile_cache_github_trending = trending_pkgs 179 | volatile_cache_github_trending_last_update_time = epochTime().int 180 | 181 | return trending_pkgs 182 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dark-color: #1d2026; 3 | } 4 | 5 | body { 6 | background-color: #f7f5ee; 7 | color: var(--dark-color); 8 | font-size: 1.3em; 9 | } 10 | 11 | .list-unstyled { 12 | list-style: none; 13 | padding: 0; 14 | margin: 0; 15 | } 16 | 17 | .logo { 18 | background: url('/img/nimble.png'); 19 | background-size: 100%; 20 | background-repeat: no-repeat; 21 | width: 70px; 22 | height: 70px; 23 | display: inline-block; 24 | } 25 | 26 | .logo.bw { 27 | filter: saturate(0); 28 | } 29 | 30 | .spotlight { 31 | position: relative; 32 | } 33 | 34 | .spotlight input { 35 | border: 2px white solid; 36 | font-size: 1.1em; 37 | border-radius: 45px; 38 | padding: 14px 20px; 39 | outline: 0; 40 | transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out; 41 | } 42 | 43 | .spotlight input:focus { 44 | border-color: lightblue; 45 | box-shadow: 0 0 0 0.25rem rgb(255 255 255 / 15%); 46 | } 47 | 48 | .spotlight:after { 49 | position: absolute; 50 | right: 35px; 51 | top: 17px; 52 | content: "/"; 53 | color: lightblue; 54 | border: 1px lightblue solid; 55 | width: 36px; 56 | border-radius: 60px; 57 | display: inline-block; 58 | text-align: center; 59 | font-size: 16px; 60 | font-weight: bold; 61 | height: 36px; 62 | line-height: 33px; 63 | } 64 | 65 | .navbar { 66 | background-color: #f7f5ee; 67 | } 68 | 69 | .tags-area { 70 | font-size: .78em; 71 | } 72 | 73 | .tags-area a { 74 | text-decoration: none; 75 | background-color: rgb(0 0 0 / 10%); 76 | color: white; 77 | display: inline-block; 78 | padding: 5px 10px 6px; 79 | border-radius: 20px; 80 | line-height: .8em; 81 | /*border: 1px white solid;*/ 82 | transition: all ease .23s; 83 | margin-bottom: 10px; 84 | } 85 | 86 | .tags-area a:hover { 87 | background-color: rgb(0 0 0 / 20%); 88 | } 89 | 90 | .box { 91 | border: 1px #EAEAEA solid; 92 | border-bottom-width: 2px; 93 | background-color: white; 94 | font-size: .85em; 95 | position: relative; 96 | } 97 | 98 | .box-pkg:after { 99 | content: attr(stars) " ★"; 100 | position: absolute; 101 | top: 15px; 102 | right: 15px; 103 | font-weight: bold; 104 | font-size: .7em; 105 | color: #daaa3f; 106 | } 107 | 108 | .box a { 109 | text-decoration: none; 110 | font-weight: 800; 111 | } 112 | 113 | .box h3 { 114 | font-size: 1.2em; 115 | font-weight: 600; 116 | } 117 | 118 | .box p { 119 | line-height: normal; 120 | } 121 | 122 | .package-box-meta-foot { 123 | margin: 10px 0 0 0; 124 | padding: 0; 125 | list-style: none; 126 | font-size: .8em; 127 | } 128 | 129 | .package-box-meta-foot a { 130 | text-decoration: none; 131 | font-weight: 500; 132 | } 133 | 134 | .package-box-meta-foot li { 135 | display: inline; 136 | margin-right: 10px; 137 | color: #212121; 138 | } 139 | 140 | .package-box-meta-foot li svg { 141 | fill: #212121; 142 | } 143 | 144 | .box { 145 | transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out; 146 | } 147 | 148 | .btn-tag { 149 | background-color: transparent; 150 | color: #62a548; 151 | border: 2px solid #62a548; 152 | border-radius: 0.6em; 153 | font-size: 1rem; 154 | text-transform: uppercase; 155 | padding: 0.2em 0.5em; 156 | } 157 | 158 | .btn-tag:hover { 159 | background-color: #62a548; 160 | color: #f7f5ee; 161 | } 162 | 163 | .pkg-btn { 164 | background-color: transparent; 165 | color: #e74c3c; 166 | border: 2px solid #e74c3c; 167 | border-radius: 0.6em; 168 | font-size: 1rem; 169 | text-transform: uppercase; 170 | /*padding: 1.2rem 2.8rem;*/ 171 | margin: 0.2em; 172 | } 173 | 174 | .pkg-btn:hover { 175 | color: #f7f5ee; 176 | background-color: #e74c3c; 177 | border: 2px solid #e74c3c; 178 | } 179 | 180 | /** 181 | * Blog 182 | */ 183 | 184 | .featured-post-thumbnail { 185 | background-size: cover; 186 | backgroun-repeat: no-repeat; 187 | background-position: center center; 188 | height: 420px; 189 | } 190 | 191 | .featured-post-card .inner-post-card { 192 | z-index: 3; 193 | } 194 | 195 | .featured-post-top .badge { 196 | font-size: 0.7rem; 197 | } 198 | 199 | .featured-post-card:after { 200 | content: ""; 201 | background-image: linear-gradient(0deg, #0c0d0f, transparent 100%); 202 | height: 100%; 203 | width: 100%; 204 | position: absolute; 205 | top: 0; 206 | left: 0; 207 | z-index: 1; 208 | } 209 | 210 | a { 211 | text-decoration: none; 212 | } 213 | 214 | article ul { 215 | padding: 0; 216 | list-style: none; 217 | } 218 | 219 | article a { 220 | font-weight: normal !important; 221 | } 222 | 223 | article p { 224 | line-height: 1.6em !important; 225 | } 226 | 227 | pre { 228 | padding: 15px; 229 | border-radius: 10px; 230 | } 231 | 232 | .dark-theme .text-theme-color { 233 | color: white; 234 | } 235 | 236 | .dark-theme article svg { 237 | fill: #8A94A7; 238 | margin-right: 10px; 239 | } 240 | 241 | .dark-theme pre { 242 | background: #1d2026; 243 | } 244 | 245 | .dark-theme .box:hover { 246 | box-shadow: 0 0 0 0.25rem #242830; 247 | } 248 | 249 | .light-theme .box:hover { 250 | box-shadow: 0 0 0 0.25rem white; 251 | border-color: white; 252 | } 253 | 254 | .package-box-meta-foot li { 255 | color: #8A94A7; 256 | } 257 | 258 | .package-box-meta-foot li svg { 259 | fill: #8A94A7; 260 | } 261 | 262 | 263 | /** 264 | * Dark Theme 265 | */ 266 | body.dark-theme { 267 | color: #f7f5ee; 268 | background-color: #1D2026; 269 | } 270 | 271 | body.dark-theme .tags-area a { 272 | background-color: #4d5054; 273 | } 274 | 275 | body.dark-theme .tags-area a:hover { 276 | background-color: #0c0c0c; 277 | } 278 | 279 | body.dark-theme .box h3 a { 280 | color: whitesmoke !important; 281 | } 282 | 283 | body.dark-theme .spotlight input { 284 | color: white; 285 | background-color: #242830; 286 | border-color: #242830; 287 | } 288 | 289 | body.dark-theme .spotlight:after { 290 | color: #444850; 291 | border-color: #444850; 292 | } 293 | 294 | body.dark-theme .navbar { 295 | background-color: var(--dark-color); 296 | } 297 | 298 | body.dark-theme .navbar-nav a { 299 | color: lightblue; 300 | } 301 | 302 | body.dark-theme #hero-landing-page { 303 | color: whitesmoke; 304 | } 305 | 306 | body.dark-theme .box { 307 | background-color: #242830; 308 | border-color: #242830; 309 | } 310 | 311 | body.dark-theme .box.box-pkg { 312 | color: #8A94A7; 313 | } 314 | 315 | body.dark-theme .text-muted { 316 | color: #8c8c8c !important; 317 | } 318 | 319 | body.dark-theme .table { 320 | color: #f7f5ee; 321 | } 322 | 323 | body #light-mode-icon { 324 | display: block; 325 | } 326 | 327 | body.dark-theme #light-mode-icon { 328 | display: none; 329 | } 330 | 331 | body #dark-mode-icon { 332 | display: none; 333 | } 334 | 335 | body.dark-theme #dark-mode-icon { 336 | display: block; 337 | } 338 | 339 | footer { 340 | font-size: 1.1rem; 341 | } 342 | 343 | footer .container { 344 | border-top: 1px solid rgba(255,255,255,.1); 345 | } 346 | 347 | footer ul { 348 | padding: 0; 349 | margin: 0; 350 | list-style: none; 351 | } 352 | 353 | footer a { 354 | color: #097dea; 355 | } 356 | 357 | .anchor { 358 | margin-right: 10px; 359 | } 360 | 361 | /** 362 | * Misc 363 | */ 364 | .pt-10 { 365 | padding-top: 6rem !important; 366 | } 367 | 368 | .py-10 { 369 | padding-top: 6rem !important; 370 | padding-bottom: 6rem !important; 371 | } 372 | 373 | .pt-32 { 374 | padding-top: 8rem !important; 375 | } 376 | 377 | .py-32 { 378 | padding-top: 8rem !important; 379 | padding-bottom: 8rem !important; 380 | } 381 | 382 | @media (max-width: 992px) { 383 | #art { display: none; } 384 | #meta-section { display: none; } 385 | } 386 | -------------------------------------------------------------------------------- /public/css/lime.min.css: -------------------------------------------------------------------------------- 1 | html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active{outline:0}a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}html{box-sizing:border-box;font-size:62.5%}@media (max-width:56.25em){html{font-size:56.25%}}*,::after,::before{margin:0;padding:0;box-sizing:inherit}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%}body{color:#383f3d;font-family:Roboto,sans-serif;font-size:1.6rem;font-weight:300;line-height:1.6;letter-spacing:.01rem}b,strong{font-weight:700}a{color:#9dd94e;text-decoration:none}a:hover{color:#006e00}hr{margin:3rem;border-top:.1rem solid #e3eae5}.row{display:flex;flex-direction:row}@media (max-width:56.25em){.row{flex-direction:col}}.col{flex:1 1 auto;margin-left:2%}.col:first-child{margin-left:0}.container{position:relative;margin:0 auto;max-width:120rem;padding:0 1rem;width:100%;box-sizing:border-box}.container.medium{max-width:100rem}.container.small{max-width:80rem}@media (min-width:25em){.container{width:90%;padding:0 1rem}}@media (min-width:34.375em){.container{width:85%;padding:0 1rem}.column,.columns{width:100%;float:left;box-sizing:border-box;margin-left:2%}.column:first-child,.columns:first-child{margin-left:0}.one.column,.one.columns{width:6.5%}.two.columns{width:15%}.three.columns{width:23.5%}.four.columns{width:32%}.five.columns{width:40.5%}.six.columns{width:49%}.seven.columns{width:57.5%}.eight.columns{width:66%}.nine.columns{width:74.5%}.ten.columns{width:83%}.eleven.columns{width:91.5%}.twelve.columns{width:100%;margin-left:0}.one-third.column{width:32%}.two-thirds.column{width:66%}.one-half.column{width:49%}.offset-by-one.column,.offset-by-one.columns{margin-left:8.5%}.offset-by-two.column,.offset-by-two.columns{margin-left:17%}.offset-by-three.column,.offset-by-three.columns{margin-left:25.5%}.offset-by-four.column,.offset-by-four.columns{margin-left:34%}.offset-by-five.column,.offset-by-five.columns{margin-left:42.5%}.offset-by-six.column,.offset-by-six.columns{margin-left:51%}.offset-by-seven.column,.offset-by-seven.columns{margin-left:59.5%}.offset-by-eight.column,.offset-by-eight.columns{margin-left:68%}.offset-by-nine.column,.offset-by-nine.columns{margin-left:76.5%}.offset-by-ten.column,.offset-by-ten.columns{margin-left:85%}.offset-by-eleven.column,.offset-by-eleven.columns{margin-left:93.5%}.offset-by-one-third.column,.offset-by-one-third.columns{margin-left:34%}.offset-by-two-thirds.column,.offset-by-two-thirds.columns{margin-left:68%}.offset-by-one-half.column{margin-left:51%}}.clearfix,.container:after,.row:after{content:"";display:table;clear:both}.button,button{margin-bottom:1rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}section{margin:2rem 0}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:2rem;font-weight:300}h1{font-size:4rem;line-height:1.2;letter-spacing:-.1rem}h2{font-size:3.6rem;line-height:1.25;letter-spacing:-.1rem}h3{font-size:3rem;line-height:1.3;letter-spacing:-.1rem}h4{font-size:2.4rem;line-height:1.35;letter-spacing:-.08rem}h5{font-size:1.8rem;line-height:1.5;letter-spacing:-.05rem}h6{font-size:1.5rem;line-height:1.6;letter-spacing:0}@media (min-width:34.375em){h1{font-size:5rem}h2{font-size:4.2rem}h3{font-size:3.6rem}h4{font-size:3rem}h5{font-size:2.4rem}h6{font-size:1.8rem}}p{margin:0 auto 2rem}.text-center{text-align:center}.text-right{text-align:right}.text-left{text-align:left}.btn:link,.btn:visited,btn,button,input[type=button],input[type=reset],input[type=submit]{background-color:transparent;border:.1rem solid #555;border-radius:1.5rem;font-size:1.6rem;appearance:none;cursor:pointer;display:inline-block;letter-spacing:.1rem;line-height:3.8rem;padding:0 2rem;text-align:center;text-transform:uppercase;text-decoration:none;vertical-align:middle;margin-bottom:1rem;color:#555}.btn:link:focus,.btn:link:hover,.btn:visited:focus,.btn:visited:hover,btn:focus,btn:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{color:#151515;border:.1rem solid #151515;outline:0}.btn:link:disabled,.btn:visited:disabled,btn:disabled,button:disabled,input[type=button]:disabled,input[type=reset]:disabled,input[type=submit]:disabled{cursor:default;opacity:.5}.btn:link:disabled:focus,.btn:link:disabled:hover,.btn:visited:disabled:focus,.btn:visited:disabled:hover,btn:disabled:focus,btn:disabled:hover,button:disabled:focus,button:disabled:hover,input[type=button]:disabled:focus,input[type=button]:disabled:hover,input[type=reset]:disabled:focus,input[type=reset]:disabled:hover,input[type=submit]:disabled:focus,input[type=submit]:disabled:hover{color:#555;border-color:#555;opacity:.5}.btn:link.btn-full,.btn:visited.btn-full,btn.btn-full,button.btn-full,input[type=button].btn-full,input[type=reset].btn-full,input[type=submit].btn-full{display:block;width:100%}code{background-color:#e3eae5;padding:.2rem .5rem .2rem .5rem;border-radius:.4rem;margin:0 .2rem}pre{background-color:#e3eae5;overflow-y:scroll;padding:1rem;margin-bottom:2rem}pre>code{padding:0;font-size:1.6rem}blockquote{border-left:.4rem solid #383f3d;padding:1rem 2rem;background-color:#e3eae5;margin-bottom:2rem}blockquote :last-of-type{margin-bottom:0}blockquote cite{font-style:italic;display:block;text-align:left}input:not([type]),input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=url],input[type=week],select,textarea{width:100%;appearance:none;background-color:transparent;box-shadow:none;border:.1rem solid #383f3d;font-size:1.6rem;border-radius:.4rem;box-sizing:inherit;height:3.8rem;padding:.6rem 1rem;margin-bottom:1.5rem;color:#383f3d}input:not([type]):focus,input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus,input[type=week]:focus,select:focus,textarea:focus{border-color:#555;outline:0}textarea{min-height:6.5rem;max-width:50vw}label,legend{display:block;font-weight:700;margin-bottom:.7rem}input[type=checkbox],input[type=radio]{display:inline;margin-bottom:1.5rem}.label-inline{display:inline-block;font-weight:400;margin-left:.5rem;margin-bottom:0}:-moz-placeholder,:-ms-input-placeholder,::-moz-placeholder,::-webkit-input-placeholder{color:#383f3d}dl,ol,ul{list-style:none;margin-top:0;padding-left:0;margin-bottom:2rem}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{margin:0 0 0 1.5rem}ol{list-style:decimal inside}ul{list-style:circle inside}dd{-webkit-margin-start:40px}dt{font-weight:700}table{width:100%;border-spacing:0;margin-bottom:3rem}td,th{text-align:left;border-bottom:.1rem solid #383f3d;padding:1rem}thead th{border-bottom:2px solid #383f3d}thead{background-color:#e3eae5} -------------------------------------------------------------------------------- /templates/base.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc base_page(req: Request, content: string): string = 3 | # result = "" 4 | 5 | 6 | 7 | Nim Package Directory 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 58 | 59 |
60 | ${content} 61 |
62 | 63 | 111 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /signatures.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Nimble package signing 3 | # 4 | # Copyright 2016 Federico Ceratto 5 | # Released under GPLv3 License, see LICENSE file 6 | # 7 | 8 | # Core developer keys -s-> roster.json --> buildbot/endorsed keys --> signed releases 9 | # Owners -sign-> signed packages.json block -id-> signed package 10 | 11 | # Nimble process: 12 | # 1) 13 | # Update roster.json 14 | # load and verify roster 15 | # load and verify packages.json 16 | # 17 | # a verify binary package .asc file using key from roster 18 | 19 | # 2) 20 | # load and verify packages.json block using owner key 21 | # 22 | # 23 | # Pkg owner workflow: 24 | # Sign packages item including authorized keys 25 | # Sign Git tags 26 | # Sign tarball? 27 | 28 | import std/[ 29 | base64, 30 | httpclient, 31 | json, 32 | os, 33 | osproc, 34 | streams, 35 | strutils, 36 | sequtils, 37 | tables, 38 | times 39 | ] 40 | import tempfile 41 | 42 | from sequtils import toSeq 43 | from algorithm import sorted 44 | 45 | const 46 | gpg_path = "/usr/bin/gpg" 47 | core_accepted_keys = @[ 48 | "0x4E6CA40C9E4B23FB", # araq 49 | "0xC9087E57971FB655", # dom 50 | ] 51 | 52 | proc to_s_ugly(result: var string, node: JsonNode) = 53 | ## Converts `node` to its JSON Representation, without 54 | ## regard for human readability. Meant to improve ``$`` string 55 | ## conversion performance. 56 | ## 57 | ## JSON representation is stored in the passed `result` 58 | ## 59 | ## This provides higher efficiency than the ``pretty`` procedure as it 60 | ## does **not** attempt to format the resulting JSON to make it human readable. 61 | var comma = false 62 | case node.kind: 63 | of JArray: 64 | result.add "[" 65 | for child in node.elems: 66 | if comma: result.add "," 67 | else: comma = true 68 | result.to_s_ugly child 69 | result.add "]" 70 | of JObject: 71 | result.add "{" 72 | for n, key_value in toSeq(pairs(node.fields)).sorted(system.cmp): 73 | let (key, value) = key_value 74 | if comma: result.add "," 75 | else: comma = true 76 | result.add key.escapeJson() 77 | result.add ":" 78 | result.to_s_ugly value 79 | result.add "}" 80 | of JString: 81 | result.add node.str.escapeJson() 82 | of JInt: 83 | result.add($node.num) 84 | of JFloat: 85 | result.add($node.fnum) 86 | of JBool: 87 | result.add(if node.bval: "true" else: "false") 88 | of JNull: 89 | result.add "null" 90 | 91 | proc serialize(node: JsonNode): string = 92 | ## Serialize node 93 | ## Keys are sorted lexicographically, the indentation is two whitespaces, 94 | ## there are no newlines and spaces at the beginning and the end. 95 | result = newStringOfCap(node.len shl 1) 96 | to_s_ugly(result, node) 97 | 98 | import strtabs 99 | 100 | proc generate_gpg_signature*(node: JsonNode, key: string): string = 101 | ## Generate GPG signature for a JSON node 102 | ## The signature is generated by GPG-signing the output of 103 | ## pretty(indent=2) 104 | ## Keys are sorted lexicographically, the indentation is two whitespaces, 105 | ## there are no newlines and spaces at the beginning and the end. 106 | let 107 | tmp_dir = mkdtemp() 108 | tmp_clearfn = tmp_dir / "cleartext" 109 | tmp_signature = tmp_dir / "signature" 110 | 111 | tmp_clearfn.writeFile(node.serialize) 112 | let tty = execProcess("/usr/bin/tty") 113 | let local_user = 114 | if key.len == 0: "" 115 | else: "--local-user $#" % [key] 116 | let cmd = "$# --detach-sign --output $# $# $#" % [gpg_path, 117 | tmp_signature, local_user, tmp_clearfn] 118 | assert dir_exists(tmp_dir) 119 | assert file_exists(tmp_clearfn) 120 | let env = newStringTable("GPG_TTY", tty, modeCaseInsensitive) 121 | echo env # FIXME use env? 122 | var cmd_out = "" 123 | try: 124 | cmd_out = execProcess(cmd)#, env=env) 125 | result = tmp_signature.readFile().encode() 126 | if key.len == 0: 127 | doAssert cmd_out.contains("default secret key for signing"), "NO DE" 128 | doAssert cmd_out.contains("signing failed") == false, "output " 129 | tmp_dir.removeDir() 130 | except: 131 | echo "signing failure: ", getCurrentExceptionMsg() 132 | echo "gpg command: ", cmd 133 | echo $dir_exists(tmp_dir) 134 | echo "## gpg output ##" 135 | echo cmd_out 136 | echo "## end ouf output ##" 137 | #tmp_dir.removeDir() 138 | raise newException(Exception, "Failed to sign:\n$#" % cmd_out) 139 | 140 | proc embed_gpg_signature*(node: JsonNode, key: string) = 141 | ## Generate a GPG signature for a JSON node 142 | ## The "signatures" key is emptied during the signing 143 | ## The new signature is added to "signatures" as (key_id, signature) 144 | let tmpnode = copy(node) 145 | if tmpnode.has_key("signatures"): 146 | tmpnode.delete("signatures") 147 | let sig = generate_gpg_signature(tmpnode, key) 148 | if not node.has_key("signatures"): 149 | node.add("signatures", newJArray()) 150 | node["signatures"].add newJString(sig) 151 | 152 | 153 | proc verify_gpg_signature*(node: JsonNode, signature: string): string = 154 | ## Verify a GPG signature on a JSON node 155 | ## Returns: key id 156 | let 157 | tmp_dir = mkdtemp() 158 | tmp_clearfn = tmp_dir / "cleartext" 159 | tmp_signature = tmp_dir / "signature" 160 | 161 | tmp_clearfn.writeFile(node.serialize) 162 | tmp_signature.writeFile(signature.decode()) 163 | let gpg = startProcess(gpg_path, args = ["--verify", tmp_signature, 164 | tmp_clearfn]) 165 | let exit_code = gpg.waitForExit(timeout=30) 166 | result = gpg.outputStream.readAll() 167 | tmp_dir.removeDir() 168 | if exit_code != 0: 169 | raise newException(Exception, "Bad signature:\n$#" % result) 170 | 171 | 172 | proc verify_gpg_signature_is_allowed*(node: JsonNode, signature: string, 173 | accepted_keys: seq[string] = @[]): string = 174 | ## Verify that a GPG signature is valid and the key belongs to the set 175 | ## of accepted keys 176 | let gpg_out = verify_gpg_signature(node, signature) 177 | 178 | var signing_key = "" 179 | for line in gpg_out.splitLines: 180 | if line.contains("using ") and line.contains(" key 0x"): 181 | let p = line.find(" key 0x") + 5 182 | signing_key = line[p..p+18].toUpperAscii 183 | 184 | for accepted_k in accepted_keys: 185 | if signing_key == accepted_k.toUpperAscii: 186 | return signing_key 187 | 188 | raise newException(Exception, "$# is not an accepted key" % signing_key) 189 | 190 | 191 | proc verify_enough_allowed_gpg_signatures*(node: JsonNode, 192 | accepted_keys: seq[string] = @[], threshold: int) = 193 | ## Verify that a JSON node is signed by enough accepted keys 194 | ## Other keys are verified and then ignored 195 | if not node.has_key("signatures"): 196 | raise newException(Exception, "Missing signatures field") 197 | let tmpnode = copy(node) 198 | tmpnode.delete("signatures") 199 | var validated_cnt = 0 200 | for sig in node["signatures"]: 201 | try: 202 | discard tmpnode.verify_gpg_signature_is_allowed( 203 | sig.str, accepted_keys) 204 | validated_cnt.inc 205 | if validated_cnt == threshold: 206 | return 207 | except: 208 | # either the signature is not valid or from a non-allowed key 209 | discard 210 | 211 | raise newException(Exception, "Not enough allowed signatures $# $#" % [$validated_cnt, $threshold]) 212 | 213 | 214 | proc load_and_verify_roster*(fname = "roster.json", 215 | accepted_keys=core_accepted_keys, required_sigs_num=2): JsonNode = 216 | ## Load and verify roster 217 | result = fname.readFile.parseJson() 218 | stdout.write "Verifying roster... " 219 | result.verify_enough_allowed_gpg_signatures(accepted_keys, 220 | required_sigs_num) 221 | echo "[OK]" 222 | 223 | 224 | proc verify_package_metadata*(node: JsonNode) = 225 | ## Verify owner[s] signature[s] on packages.json item 226 | stdout.write "Verifying metadata... " 227 | let 228 | owners_keys = node["owner_keys"].mapIt(it.str) 229 | sigs = node["signatures"].mapIt(it.str) 230 | 231 | for sig in sigs: 232 | discard node.verify_gpg_signature_is_allowed(sig, owners_keys) 233 | echo "[OK]" 234 | return 235 | 236 | raise newException(Exception, "No valid owner signature found") 237 | 238 | 239 | proc download_file*(url, fname: string, check_modified_time=true, 240 | timeout=30): bool = 241 | ## Download file, if needed 242 | if fileExists(fname) and check_modified_time: 243 | let 244 | creation_time = fname.getFileInfo.creationTime 245 | tstamp = creation_time.format("ddd, dd MMM yyyy hh:mm:ss") & " GMT" 246 | # If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT 247 | let hc = newHttpClient() 248 | hc.headers["If-Modified-Since:"] = tstamp 249 | #hc.timeout = timeout 250 | let resp = hc.request(url) 251 | if resp.status == "304": 252 | return false 253 | writeFile(fname, resp.body) 254 | return true 255 | else: 256 | # echo "Fetching ", url 257 | writeFile(fname, newHttpClient().getContent(url)) 258 | return true 259 | 260 | 261 | 262 | -------------------------------------------------------------------------------- /templates/home.tmpl: -------------------------------------------------------------------------------- 1 | #? stdtmpl | standard 2 | #proc generate_search_box(value=""): string = 3 | # result = "" 4 |
5 |
6 |
7 |

Discover Nim's ecosystem of libraries and tools

8 |
9 |
10 | 13 |
14 |
15 |
16 | embedded systems 17 | web frameworks 18 | IoT 19 | game engines 20 | command line 21 | parser 22 | AI 23 | network 24 | yaml 25 | frontend 26 | template engines 27 | javascript 28 | wrapper 29 | graphics 30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 | 48 | 49 | #end 50 | # 51 | #proc generate_home_page(most_queried_packages, new_packages: seq[Pkg], github_trending: seq[JsonNode]): string = 52 | # result = "" 53 | 54 | ${generate_search_box()} 55 | 56 |
57 |
58 |
59 |

Trending Packages

60 |
61 |
62 |
63 | # for i in 0 .. min(9, github_trending.len)-1: 64 | # let pkg = github_trending[i] 65 |
66 |
67 |

${pkg["name"].str}

68 |

${pkg["description"].str}

69 | 83 |
84 |
85 | # end 86 |
87 |
88 | 89 |
90 |
91 |
92 |

New Packages

93 |
94 |
95 |
96 | # for i in 0 .. min(9, new_packages.len)-1: 97 | # let pkg = new_packages[i] 98 |
99 |
100 |

${pkg["name"].str}

101 |

${pkg["description"].str}

102 |
    103 |
  • 104 | 105 | # let url = pkg["url"].str.strip(false, true, {'/'}).rsplit('/', maxsplit=2) 106 | # if url.len == 3: 107 | ${url[1]} 108 | # else: 109 | Unknown 110 | # end 111 |
  • 112 |
  • 113 | 114 | Repository 115 |
  • 116 |
  • 117 | 118 | ${pkg["license"].str} 119 |
  • 120 |
121 |
122 |
123 | # end 124 |
125 |
126 | 127 |
128 |
129 |
130 |

Most Queried

131 |
132 |
133 |
134 | # for i in 0 .. min(9, most_queried_packages.len)-1: 135 | # let pkg = most_queried_packages[i] 136 |
137 |
138 |

${pkg["name"].str}

139 |

${pkg["description"].str}

140 |
    141 |
  • 142 | 143 | # let url = pkg["url"].str.strip(false, true, {'/'}).rsplit('/', maxsplit=2) 144 | # if url.len == 3: 145 | ${url[1]} 146 | # else: 147 | Unknown 148 | # end 149 |
  • 150 |
  • 151 | 152 | Repository 153 |
  • 154 |
  • 155 | 156 | ${pkg["license"].str} 157 |
  • 158 |
159 |
160 |
161 | # end 162 |
163 |
164 | -------------------------------------------------------------------------------- /package_directory.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Nimble package directory 3 | # 4 | # Copyright 2016-2023 Federico Ceratto and other contributors 5 | # Released under GPLv3 License, see LICENSE file 6 | # 7 | 8 | import std/[ 9 | asyncdispatch, 10 | deques, 11 | httpclient, 12 | httpcore, 13 | json, 14 | os, 15 | sequtils, 16 | sets, 17 | streams, 18 | strutils, 19 | tables, 20 | times, 21 | uri 22 | ] 23 | 24 | from std/xmltree import escape 25 | from std/algorithm import sort, sorted, sortedByIt, reversed 26 | from std/marshal import store, load 27 | from std/posix import onSignal, SIGINT, SIGTERM, getpid 28 | 29 | #from nimblepkg import getTagsListRemote, getVersionList 30 | import jester, 31 | morelogging, 32 | sdnotify, 33 | statsd_client 34 | 35 | import github, util, signatures, persist 36 | 37 | const 38 | nimble_packages_polling_time_s = 10 * 60 39 | sdnotify_ping_time_s = 10 40 | cache_fn = ".cache.json" 41 | 42 | 43 | # init 44 | 45 | type 46 | RssItem = object 47 | title, desc, pub_date: string 48 | url, guid: Uri 49 | 50 | # the pkg name is normalized 51 | var pkgs: Pkgs = newTable[string, Pkg]() 52 | 53 | # tag -> package name 54 | # initialized/updated by load_packages 55 | var packages_by_tag = newTable[string, seq[string]]() 56 | 57 | # word -> package name 58 | # initialized/updated by load_packages 59 | var packages_by_description_word = newTable[string, seq[string]]() 60 | 61 | # package access statistics 62 | # volatile 63 | var most_queried_packages = initCountTable[string]() 64 | 65 | 66 | type 67 | PkgHistoryItem = object 68 | name: string 69 | first_seen_time: Time 70 | 71 | Cache = object of RootObj 72 | # package creation/update history - new ones at bottom 73 | pkgs_history: seq[PkgHistoryItem] 74 | # pkgs list. Extra data from GH is embedded 75 | #pkgs: TableRef[string, Pkg] 76 | 77 | var cache: Cache 78 | 79 | proc save(cache: Cache) = 80 | let f = newFileStream(cache_fn, fmWrite) 81 | log_debug "writing " & absolutePath(cache_fn) 82 | f.store(cache) 83 | f.close() 84 | 85 | proc load_cache(): Cache = 86 | ## Load cache from disk or create empty cache 87 | log_debug "loading cache at $#" % cache_fn 88 | try: 89 | # FIXME 90 | #result.pkgs = newTable[string, Pkg]() 91 | result.pkgs_history = @[] 92 | load(newFileStream(cache_fn, fmRead), result) 93 | log_debug "cache loaded" 94 | except: 95 | log_info "initializing new cache" 96 | #result.pkgs = newTable[string, Pkg]() 97 | result.pkgs_history = @[] 98 | result.save() 99 | log_debug "new cache created" 100 | 101 | 102 | 103 | # HTML templates 104 | 105 | include "templates/base.tmpl" 106 | include "templates/home.tmpl" 107 | include "templates/pkg.tmpl" 108 | include "templates/pkg_list.tmpl" 109 | include "templates/rss.tmpl" 110 | 111 | 112 | proc search_packages*(query: string): CountTable[string] = 113 | ## Search packages by name, tag and keyword 114 | let query = query.strip.toLowerAscii.split({' ', ','}) 115 | var found_pkg_names = initCountTable[string]() 116 | for item in query: 117 | 118 | # matching by pkg name, weighted for full or partial match 119 | for pn in pkgs.keys(): 120 | if item.normalize() == pn: 121 | found_pkg_names.inc(pn, val = 5) 122 | elif pn.contains(item.normalize()): 123 | found_pkg_names.inc(pn, val = 3) 124 | 125 | if packages_by_tag.hasKey(item): 126 | for pn in packages_by_tag[item]: 127 | # matching by tags is weighted more than by word 128 | found_pkg_names.inc(pn, val = 3) 129 | 130 | # matching by description, weighted 1 131 | if packages_by_description_word.hasKey(item.toLowerAscii): 132 | for pn in packages_by_description_word[item.toLowerAscii]: 133 | found_pkg_names.inc(pn, val = 1) 134 | 135 | # sort packages by best match 136 | found_pkg_names.sort() 137 | return found_pkg_names 138 | 139 | 140 | proc load_packages*() = 141 | ## Load packages.json 142 | ## Rebuild packages_by_tag, packages_by_description_word 143 | log_debug "loading $#" % conf.packages_list_fname 144 | pkgs.clear() 145 | if not conf.packages_list_fname.file_exists: 146 | log_info "packages list file not found. First run?" 147 | let new_pkg_raw = waitFor fetch_nimble_packages() 148 | log_info "writing $#" % absolutePath(conf.packages_list_fname) 149 | conf.packages_list_fname.writeFile(new_pkg_raw) 150 | 151 | let pkg_list = conf.packages_list_fname.parseFile 152 | for pdata in pkg_list: 153 | if not pdata.hasKey("name"): 154 | continue 155 | if not pdata.hasKey("tags"): 156 | continue 157 | # Normalize pkg name 158 | pdata["name"].str = pdata["name"].str.normalize() 159 | if pdata["name"].str in pkgs: 160 | log.warn "Duplicate pkg name $#" % pdata["name"].str 161 | continue 162 | 163 | pkgs[pdata["name"].str] = pdata 164 | 165 | for tag in pdata["tags"]: 166 | if not packages_by_tag.hasKey(tag.str): 167 | packages_by_tag[tag.str] = @[] 168 | packages_by_tag[tag.str].add pdata["name"].str 169 | 170 | # collect packages matching a word in their descriptions 171 | let orig_words = pdata["description"].str.split({' ', ','}) 172 | for orig_word in orig_words: 173 | if orig_word.len < 3: 174 | continue # ignore short words 175 | let word = orig_word.toLowerAscii 176 | if not packages_by_description_word.hasKey(word): 177 | packages_by_description_word[word] = @[] 178 | packages_by_description_word[word].add pdata["name"].str 179 | 180 | log_info "Loaded ", $pkgs.len, " packages" 181 | 182 | # log_debug "writing $#" % conf.packages_list_fname 183 | # conf.packages_list_fname.writeFile(conf.packages_list_fname.readFile) 184 | 185 | 186 | proc translate_term_colors*(outp: string): string = 187 | ## Translate terminal colors into HTML with CSS classes 188 | const sequences = @[ 189 | ("", ""), 190 | ("", """"""), 191 | ("", """"""), 192 | ("", """"""), 193 | ("", """"""), 194 | ("", ""), 195 | ("", ""), 196 | ("", ""), 197 | ("", ""), 198 | ("", ""), 199 | ("", ""), 200 | ("", ""), 201 | ("", """"""), 202 | ] 203 | result = outp 204 | for s in sequences: 205 | result = result.replace(s[0], s[1]) 206 | 207 | proc sorted*[T](t: CountTable[T]): CountTable[T] = 208 | ## Return sorted CountTable 209 | var tcopy = t 210 | tcopy.sort() 211 | tcopy 212 | 213 | proc top_keys*[T](t: CountTable[T], n: int): seq[T] = 214 | ## Return CountTable most common keys 215 | result = @[] 216 | var tcopy = t 217 | tcopy.sort() 218 | for k in keys(tcopy): 219 | result.add k 220 | if result.len == n: 221 | return 222 | 223 | 224 | # Jester settings 225 | 226 | settings: 227 | port = conf.port 228 | 229 | # routes 230 | 231 | router mainRouter: 232 | 233 | get "/about.html": 234 | include "templates/about.tmpl" 235 | resp base_page(request, generate_about_page()) 236 | 237 | get "/": 238 | log_req request 239 | stats.incr("views") 240 | 241 | # Grab the most queried packages 242 | var top_pkgs: seq[Pkg] = @[] 243 | for pname in top_keys(most_queried_packages, 5): 244 | if pkgs.hasKey(pname): 245 | top_pkgs.add pkgs[pname] 246 | 247 | # Grab the newest packages 248 | log_debug "pkgs history len: $#" % $cache.pkgs_history.len 249 | var new_pkgs: seq[Pkg] = @[] 250 | for n in 1..min(cache.pkgs_history.len, 10): 251 | let package_name: string = 252 | if cache.pkgs_history[^n].name.len > 4 and cache.pkgs_history[^n].name[ 253 | 0..3] == "nim-": 254 | cache.pkgs_history[^n].name[4..^1].normalize() 255 | else: 256 | cache.pkgs_history[^n].name.normalize() 257 | if pkgs.hasKey(package_name): 258 | new_pkgs.add pkgs[package_name] 259 | else: 260 | log_debug "$# not found in package list" % package_name 261 | 262 | # Grab trending packages, as measured by GitHub 263 | let trending_pkgs = await fetch_trending_packages(request, pkgs) 264 | 265 | resp base_page(request, generate_home_page(top_pkgs, new_pkgs, 266 | trending_pkgs)) 267 | 268 | get "/search": 269 | log_req request 270 | stats.incr("views") 271 | 272 | var searched_pkgs: seq[Pkg] = @[] 273 | for name in search_packages(@"query").keys(): 274 | searched_pkgs.add pkgs[name] 275 | stats.gauge("search_found_pkgs", searched_pkgs.len) 276 | 277 | let body = generate_search_box(@"query") & generate_pkg_list_page(searched_pkgs) 278 | resp base_page(request, body) 279 | 280 | get "/pkg/@pkg_name/?": 281 | log_req request 282 | stats.incr("views") 283 | let pname = normalize(@"pkg_name") 284 | if not pkgs.hasKey(pname): 285 | resp base_page(request, "Package not found") 286 | 287 | most_queried_packages.inc pname 288 | 289 | let pkg = pkgs[pname] 290 | let url = pkg["url"].str 291 | if url.startswith("https://github.com/") or url.startswith("http://github.com/"): 292 | if not pkg.hasKey("github_last_update_time") or pkg["github_last_update_time"].num + 293 | github_caching_time < epochTime().int: 294 | # pkg is on GitHub and needs updating 295 | pkg["github_last_update_time"] = newJInt epochTime().int 296 | let owner = url.split('/')[3] 297 | let repo_name = url.split('/')[4] 298 | pkg["github_owner"] = newJString owner 299 | pkg["github_readme"] = await fetch_github_readme(owner, repo_name) 300 | pkg["doc"] = await fetch_github_doc_pages(owner, repo_name) 301 | await pkg.fetch_github_versions(owner, repo_name) 302 | 303 | resp base_page(request, generate_pkg_page(pkg)) 304 | 305 | post "/update_package": 306 | ## Create or update a package description 307 | log_req request 308 | stats.incr("views") 309 | const required_fields = @["name", "url", "method", "tags", "description", 310 | "license", "web", "signatures", "authorized_keys"] 311 | var pkg_data: JsonNode 312 | try: 313 | pkg_data = parseJson(request.body) 314 | except: 315 | log_info "Unable to parse JSON payload" 316 | halt Http400, "Unable to parse JSON payload" 317 | 318 | for field in required_fields: 319 | if not pkg_data.hasKey(field): 320 | log_info "Missing required field $#" % field 321 | halt Http400, "Missing required field $#" % field 322 | 323 | let signature = pkg_data["signatures"][0].str 324 | 325 | try: 326 | let pkg_data_copy = pkg_data.copy() 327 | pkg_data_copy.delete("signatures") 328 | let key_id = verify_gpg_signature(pkg_data_copy, signature) 329 | log_info "received key", key_id 330 | except: 331 | log_info "Invalid signature" 332 | halt Http400, "Invalid signature" 333 | 334 | let name = pkg_data["name"].str 335 | 336 | # TODO: locking 337 | load_packages() 338 | 339 | # the package exists with identical name 340 | let pkg_already_exists = pkgs.hasKey(name) 341 | 342 | if not pkg_already_exists: 343 | # scan for naming collisions 344 | let norm_name = name.normalize() 345 | for existing_pn in pkgs.keys(): 346 | if norm_name == existing_pn.normalize(): 347 | log.info "Another package named $# already exists" % existing_pn 348 | halt Http400, "Another package named $# already exists" % existing_pn 349 | 350 | if pkg_already_exists: 351 | try: 352 | let old_keys = pkgs[name]["authorized_keys"].getElems.mapIt(it.str) 353 | let pkg_data_copy = pkg_data.copy() 354 | pkg_data_copy.delete("signatures") 355 | let key_id = verify_gpg_signature_is_allowed(pkg_data_copy, signature, old_keys) 356 | log_info "$# updating package $#" % [key_id, name] 357 | except: 358 | log_info "Key not accepted" 359 | halt Http400, "Key not accepted" 360 | 361 | pkgs[name] = pkg_data 362 | 363 | var new_pkgs = newJArray() 364 | for pname in toSeq(pkgs.keys()).sorted(system.cmp): 365 | new_pkgs.add pkgs[pname] 366 | conf.packages_list_fname.writeFile(new_pkgs.pretty.cleanup_whitespace) 367 | 368 | log_info if pkg_already_exists: "Updated existing package $#" % name 369 | else: "Added new package $#" % name 370 | resp base_page(request, "OK") 371 | 372 | get "/packages.json": 373 | ## Serve the packages list file 374 | log_req request 375 | stats.incr("views") 376 | resp conf.packages_list_fname.readFile 377 | 378 | get "/api/v1/package_count": 379 | ## Serve the package count 380 | log_req request 381 | stats.incr("views") 382 | resp $pkgs.len 383 | 384 | get "/packages.xml": 385 | ## New and updated packages feed 386 | log_req request 387 | stats.incr("views_rss") 388 | let baseurl = conf.public_baseurl.parseUri 389 | let url = baseurl / "packages.xml" 390 | 391 | var rss_items: seq[RssItem] = @[] 392 | for item in cache.pkgs_history: 393 | let pn = item.name.normalize() 394 | if not pkgs.hasKey(pn): 395 | #log_debug "skipping $#" % pn 396 | continue 397 | 398 | let pkg = pkgs[pn] 399 | let item_url = baseurl / "pkg" / pn 400 | let i = RssItem( 401 | title: pn, 402 | desc: xmltree.escape(pkg["description"].str), 403 | url: item_url, 404 | guid: item_url, 405 | pub_date: $item.first_seen_time.utc.format("ddd, dd MMM yyyy hh:mm:ss zz") 406 | ) 407 | rss_items.add i 408 | 409 | let r = generate_rss_feed( 410 | title = "Nim packages", 411 | desc = "New and updated Nim packages", 412 | url = url, 413 | build_date = getTime().utc.format("ddd, dd MMM yyyy hh:mm:ss zz"), 414 | pub_date = getTime().utc.format("ddd, dd MMM yyyy hh:mm:ss zz"), 415 | ttl = 3600, 416 | rss_items 417 | ) 418 | resp(r, contentType = "application/rss+xml") 419 | 420 | get "/stats": 421 | log_req request 422 | stats.incr("views") 423 | resp base_page(request, """ 424 |
425 |

Runtime: $#

426 |

Queried packages count: $#

427 |
428 | """ % [$cpuTime(), $len(most_queried_packages)]) 429 | 430 | get "/robots.txt": 431 | ## Serve robots.txt to throttle bots 432 | const robots = """ 433 | User-agent: DataForSeoBot 434 | Disallow: / 435 | 436 | User-agent: * 437 | Disallow: /about.html 438 | Disallow: /api 439 | Disallow: /ci 440 | Disallow: /docs 441 | Disallow: /pkg 442 | Disallow: /search 443 | Disallow: /searchitem 444 | Crawl-delay: 300 445 | """ 446 | resp(robots, contentType = "text/plain") 447 | 448 | 449 | proc run_systemd_sdnotify_pinger(ping_time_s: int) {.async.} = 450 | ## Ping systemd watchdog using sd_notify 451 | const msg = "NOTIFY_SOCKET env var not found - pinging to logfile" 452 | if not existsEnv("NOTIFY_SOCKET"): 453 | log_info msg 454 | echo msg 455 | while true: 456 | log_debug "*ping*" 457 | await sleepAsync ping_time_s * 1000 458 | # never break 459 | 460 | let sd = newSDNotify() 461 | sd.notify_ready() 462 | sd.notify_main_pid(getpid()) 463 | while true: 464 | sd.ping_watchdog() 465 | await sleepAsync ping_time_s * 1000 466 | 467 | 468 | proc poll_nimble_packages(poll_time_s: int) {.async.} = 469 | ## Poll GitHub for packages.json 470 | ## Overwrites the packages.json local file! 471 | log_debug "starting GH packages.json polling" 472 | var first_run = true 473 | while true: 474 | if first_run: 475 | first_run = false 476 | else: 477 | await sleepAsync poll_time_s * 1000 478 | log_debug "Polling GitHub packages.json" 479 | try: 480 | let new_pkg_raw = await fetch_nimble_packages() 481 | if new_pkg_raw == conf.packages_list_fname.readFile: 482 | log_debug "No changes" 483 | stats.gauge("packages_all_known", pkgs.len) 484 | stats.gauge("packages_history", cache.pkgs_history.len) 485 | continue 486 | 487 | for pdata in new_pkg_raw.parseJson: 488 | if pdata.hasKey("name"): 489 | let pname = pdata["name"].str.normalize() 490 | if not pkgs.hasKey(pname): 491 | cache.pkgs_history.add PkgHistoryItem(name: pname, 492 | first_seen_time: getTime()) 493 | log_debug "New pkg added on GH: $#" % pname 494 | 495 | cache.save() 496 | log_debug "writing $#" % (getCurrentDir() / conf.packages_list_fname) 497 | conf.packages_list_fname.writeFile(new_pkg_raw) 498 | load_packages() 499 | 500 | for item in cache.pkgs_history: 501 | let pname = item.name.normalize() 502 | if not pkgs.hasKey(pname): 503 | log_debug "$# is gone" % pname 504 | 505 | stats.gauge("packages_all_known", pkgs.len) 506 | stats.gauge("packages_history", cache.pkgs_history.len) 507 | 508 | except: 509 | log.error getCurrentExceptionMsg() 510 | 511 | 512 | onSignal(SIGINT, SIGTERM): 513 | ## Exit signal handler 514 | log.info "Exiting" 515 | cache.save() 516 | quit() 517 | 518 | 519 | proc main() = 520 | #setup_seccomp() 521 | log_info "starting" 522 | conf.tmp_nimble_root_dir.createDir() 523 | load_packages() 524 | cache = load_cache() 525 | asyncCheck run_systemd_sdnotify_pinger(sdnotify_ping_time_s) 526 | asyncCheck poll_nimble_packages(nimble_packages_polling_time_s) 527 | 528 | log_info "starting server" 529 | var server = initJester(mainRouter) 530 | server.serve() 531 | 532 | when isMainModule: 533 | main() 534 | -------------------------------------------------------------------------------- /public/css/nimdoc.out.css: -------------------------------------------------------------------------------- 1 | /* 2 | Stylesheet for use with Docutils/rst2html. 3 | 4 | See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to 5 | customize this style sheet. 6 | 7 | Modified from Chad Skeeters' rst2html-style 8 | https://bitbucket.org/cskeeters/rst2html-style/ 9 | 10 | Modified by Boyd Greenfield and narimiran 11 | */ 12 | 13 | :root { 14 | --primary-background: #fff; 15 | --secondary-background: ghostwhite; 16 | --third-background: #e8e8e8; 17 | --border: #dde; 18 | --text: #222; 19 | --anchor: #07b; 20 | --anchor-focus: #607c9f; 21 | --input-focus: #1fa0eb; 22 | --strong: #3c3c3c; 23 | --hint: #9A9A9A; 24 | --nim-sprite-base64: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAN4AAAA9CAYAAADCt9ebAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFFmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDggNzkuMTY0MDM2LCAyMDE5LzA4LzEzLTAxOjA2OjU3ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTEyLTAzVDAxOjAzOjQ4KzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0xMi0wM1QwMjoyODo0MSswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0xMi0wM1QwMjoyODo0MSswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozMzM0ZjAxYS0yMDExLWE1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"); 25 | 26 | --keyword: #5e8f60; 27 | --identifier: #222; 28 | --comment: #484a86; 29 | --operator: #155da4; 30 | --punctuation: black; 31 | --other: black; 32 | --escapeSequence: #c4891b; 33 | --number: #252dbe; 34 | --literal: #a4255b; 35 | --raw-data: #a4255b; 36 | } 37 | 38 | [data-theme="dark"] { 39 | --primary-background: #171921; 40 | --secondary-background: #1e202a; 41 | --third-background: #2b2e3b; 42 | --border: #0e1014; 43 | --text: #fff; 44 | --anchor: #8be9fd; 45 | --anchor-focus: #8be9fd; 46 | --input-focus: #8be9fd; 47 | --strong: #bd93f9; 48 | --hint: #7A7C85; 49 | --nim-sprite-base64: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARMAAABMCAYAAABOBlMuAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFFmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDggNzkuMTY0MDM2LCAyMDE5LzA4LzEzLTAxOjA2OjU3ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTEyLTAzVDAxOjE4OjIyKzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0xMi0wM1QwMToyMDoxMCswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0xMi0wM1QwMToyMDoxMCswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDplZGViMzU3MC1iNmZjLWQy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"); 50 | 51 | --keyword: #ff79c6; 52 | --identifier: #f8f8f2; 53 | --comment: #6272a4; 54 | --operator: #ff79c6; 55 | --punctuation: #f8f8f2; 56 | --other: #f8f8f2; 57 | --escapeSequence: #bd93f9; 58 | --number: #bd93f9; 59 | --literal: #f1fa8c; 60 | --raw-data: #8be9fd; 61 | } 62 | 63 | .theme-switch-wrapper { 64 | display: flex; 65 | align-items: center; 66 | 67 | em { 68 | margin-left: 10px; 69 | font-size: 1rem; 70 | } 71 | } 72 | .theme-switch { 73 | display: inline-block; 74 | height: 22px; 75 | position: relative; 76 | width: 50px; 77 | } 78 | 79 | .theme-switch input { 80 | display: none; 81 | } 82 | 83 | .slider { 84 | background-color: #ccc; 85 | bottom: 0; 86 | cursor: pointer; 87 | left: 0; 88 | position: absolute; 89 | right: 0; 90 | top: 0; 91 | transition: .4s; 92 | } 93 | 94 | .slider:before { 95 | background-color: #fff; 96 | bottom: 4px; 97 | content: ""; 98 | height: 13px; 99 | left: 4px; 100 | position: absolute; 101 | transition: .4s; 102 | width: 13px; 103 | } 104 | 105 | input:checked + .slider { 106 | background-color: #66bb6a; 107 | } 108 | 109 | input:checked + .slider:before { 110 | transform: translateX(26px); 111 | } 112 | 113 | .slider.round { 114 | border-radius: 17px; 115 | } 116 | 117 | .slider.round:before { 118 | border-radius: 50%; 119 | } 120 | 121 | html { 122 | font-size: 100%; 123 | -webkit-text-size-adjust: 100%; 124 | -ms-text-size-adjust: 100%; } 125 | 126 | body { 127 | font-family: "Lato", "Helvetica Neue", "HelveticaNeue", Helvetica, Arial, sans-serif; 128 | font-weight: 400; 129 | font-size: 1.125em; 130 | line-height: 1.5; 131 | color: var(--text); 132 | background-color: var(--primary-background); } 133 | 134 | /* Skeleton grid */ 135 | .container { 136 | position: relative; 137 | width: 100%; 138 | max-width: 1050px; 139 | margin: 0 auto; 140 | padding: 0; 141 | box-sizing: border-box; } 142 | 143 | .column, 144 | .columns { 145 | width: 100%; 146 | float: left; 147 | box-sizing: border-box; 148 | margin-left: 1%; 149 | } 150 | 151 | .column:first-child, 152 | .columns:first-child { 153 | margin-left: 0; } 154 | 155 | .three.columns { 156 | width: 19%; } 157 | 158 | .nine.columns { 159 | width: 80.0%; } 160 | 161 | .twelve.columns { 162 | width: 100%; 163 | margin-left: 0; } 164 | 165 | @media screen and (max-width: 860px) { 166 | .three.columns { 167 | display: none; 168 | } 169 | .nine.columns { 170 | width: 98.0%; 171 | } 172 | body { 173 | font-size: 1em; 174 | line-height: 1.35; 175 | } 176 | } 177 | 178 | cite { 179 | font-style: italic !important; } 180 | 181 | 182 | /* Nim search input */ 183 | div#searchInputDiv { 184 | margin-bottom: 1em; 185 | } 186 | input#searchInput { 187 | width: 80%; 188 | } 189 | 190 | /* 191 | * Some custom formatting for input forms. 192 | * This also fixes input form colors on Firefox with a dark system theme on Linux. 193 | */ 194 | input { 195 | -moz-appearance: none; 196 | background-color: var(--secondary-background); 197 | color: var(--text); 198 | border: 1px solid var(--border); 199 | font-family: "Lato", "Helvetica Neue", "HelveticaNeue", Helvetica, Arial, sans-serif; 200 | font-size: 0.9em; 201 | padding: 6px; 202 | } 203 | 204 | input:focus { 205 | border: 1px solid var(--input-focus); 206 | box-shadow: 0 0 3px var(--input-focus); 207 | } 208 | 209 | select { 210 | -moz-appearance: none; 211 | background-color: var(--secondary-background); 212 | color: var(--text); 213 | border: 1px solid var(--border); 214 | font-family: "Lato", "Helvetica Neue", "HelveticaNeue", Helvetica, Arial, sans-serif; 215 | font-size: 0.9em; 216 | padding: 6px; 217 | } 218 | 219 | select:focus { 220 | border: 1px solid var(--input-focus); 221 | box-shadow: 0 0 3px var(--input-focus); 222 | } 223 | 224 | /* Docgen styles */ 225 | /* Links */ 226 | a { 227 | color: var(--anchor); 228 | text-decoration: none; 229 | } 230 | 231 | a span.Identifier { 232 | text-decoration: underline; 233 | text-decoration-color: #aab; 234 | } 235 | 236 | a.reference-toplevel { 237 | font-weight: bold; 238 | } 239 | 240 | a.toc-backref { 241 | text-decoration: none; 242 | color: var(--text); } 243 | 244 | a.link-seesrc { 245 | color: #607c9f; 246 | font-size: 0.9em; 247 | font-style: italic; } 248 | 249 | a:hover, 250 | a:focus { 251 | color: var(--anchor-focus); 252 | text-decoration: underline; } 253 | 254 | a:hover span.Identifier { 255 | color: var(--anchor); 256 | } 257 | 258 | 259 | sub, 260 | sup { 261 | position: relative; 262 | font-size: 75%; 263 | line-height: 0; 264 | vertical-align: baseline; } 265 | 266 | sup { 267 | top: -0.5em; } 268 | 269 | sub { 270 | bottom: -0.25em; } 271 | 272 | img { 273 | width: auto; 274 | height: auto; 275 | max-width: 100%; 276 | vertical-align: middle; 277 | border: 0; 278 | -ms-interpolation-mode: bicubic; } 279 | 280 | @media print { 281 | * { 282 | color: black !important; 283 | text-shadow: none !important; 284 | background: transparent !important; 285 | box-shadow: none !important; } 286 | 287 | a, 288 | a:visited { 289 | text-decoration: underline; } 290 | 291 | a[href]:after { 292 | content: " (" attr(href) ")"; } 293 | 294 | abbr[title]:after { 295 | content: " (" attr(title) ")"; } 296 | 297 | .ir a:after, 298 | a[href^="javascript:"]:after, 299 | a[href^="#"]:after { 300 | content: ""; } 301 | 302 | pre, 303 | blockquote { 304 | border: 1px solid #999; 305 | page-break-inside: avoid; } 306 | 307 | thead { 308 | display: table-header-group; } 309 | 310 | tr, 311 | img { 312 | page-break-inside: avoid; } 313 | 314 | img { 315 | max-width: 100% !important; } 316 | 317 | @page { 318 | margin: 0.5cm; } 319 | 320 | h1 { 321 | page-break-before: always; } 322 | 323 | h1.title { 324 | page-break-before: avoid; } 325 | 326 | p, 327 | h2, 328 | h3 { 329 | orphans: 3; 330 | widows: 3; } 331 | 332 | h2, 333 | h3 { 334 | page-break-after: avoid; } 335 | } 336 | 337 | 338 | p { 339 | margin-top: 0.5em; 340 | margin-bottom: 0.5em; 341 | } 342 | 343 | small { 344 | font-size: 85%; } 345 | 346 | strong { 347 | font-weight: 600; 348 | font-size: 0.95em; 349 | color: var(--strong); 350 | } 351 | 352 | em { 353 | font-style: italic; } 354 | 355 | h1 { 356 | font-size: 1.8em; 357 | font-weight: 400; 358 | padding-bottom: .25em; 359 | border-bottom: 6px solid var(--third-background); 360 | margin-top: 2.5em; 361 | margin-bottom: 1em; 362 | line-height: 1.2em; } 363 | 364 | h1.title { 365 | padding-bottom: 1em; 366 | border-bottom: 0px; 367 | font-size: 2.5em; 368 | text-align: center; 369 | font-weight: 900; 370 | margin-top: 0.75em; 371 | margin-bottom: 0em; 372 | } 373 | 374 | h2 { 375 | font-size: 1.3em; 376 | margin-top: 2em; } 377 | 378 | h2.subtitle { 379 | text-align: center; } 380 | 381 | h3 { 382 | font-size: 1.125em; 383 | font-style: italic; 384 | margin-top: 1.5em; } 385 | 386 | h4 { 387 | font-size: 1.125em; 388 | margin-top: 1em; } 389 | 390 | h5 { 391 | font-size: 1.125em; 392 | margin-top: 0.75em; } 393 | 394 | h6 { 395 | font-size: 1.1em; } 396 | 397 | 398 | ul, 399 | ol { 400 | padding: 0; 401 | margin-top: 0.5em; 402 | margin-left: 0.75em; } 403 | 404 | ul ul, 405 | ul ol, 406 | ol ol, 407 | ol ul { 408 | margin-bottom: 0; 409 | margin-left: 1.25em; } 410 | 411 | li { 412 | list-style-type: circle; 413 | } 414 | 415 | ul.simple-boot li { 416 | list-style-type: none; 417 | margin-left: 0em; 418 | margin-bottom: 0.5em; 419 | } 420 | 421 | ol.simple > li, ul.simple > li { 422 | margin-bottom: 0.25em; 423 | margin-left: 0.4em } 424 | 425 | ul.simple.simple-toc > li { 426 | margin-top: 1em; 427 | } 428 | 429 | ul.simple-toc { 430 | list-style: none; 431 | font-size: 0.9em; 432 | margin-left: -0.3em; 433 | margin-top: 1em; } 434 | 435 | ul.simple-toc > li { 436 | list-style-type: none; 437 | } 438 | 439 | ul.simple-toc-section { 440 | list-style-type: circle; 441 | margin-left: 1em; 442 | color: #6c9aae; } 443 | 444 | 445 | ol.arabic { 446 | list-style: decimal; } 447 | 448 | ol.loweralpha { 449 | list-style: lower-alpha; } 450 | 451 | ol.upperalpha { 452 | list-style: upper-alpha; } 453 | 454 | ol.lowerroman { 455 | list-style: lower-roman; } 456 | 457 | ol.upperroman { 458 | list-style: upper-roman; } 459 | 460 | ul.auto-toc { 461 | list-style-type: none; } 462 | 463 | 464 | dl { 465 | margin-bottom: 1.5em; } 466 | 467 | dt { 468 | margin-bottom: -0.5em; 469 | margin-left: 0.0em; } 470 | 471 | dd { 472 | margin-left: 2.0em; 473 | margin-bottom: 3.0em; 474 | margin-top: 0.5em; } 475 | 476 | 477 | hr { 478 | margin: 2em 0; 479 | border: 0; 480 | border-top: 1px solid #aaa; } 481 | 482 | blockquote { 483 | font-size: 0.9em; 484 | font-style: italic; 485 | padding-left: 0.5em; 486 | margin-left: 0; 487 | border-left: 5px solid #bbc; 488 | } 489 | 490 | .pre { 491 | font-family: "Source Code Pro", Monaco, Menlo, Consolas, "Courier New", monospace; 492 | font-weight: 500; 493 | font-size: 0.85em; 494 | color: var(--text); 495 | background-color: var(--third-background); 496 | padding-left: 3px; 497 | padding-right: 3px; 498 | border-radius: 4px; 499 | } 500 | 501 | pre { 502 | font-family: "Source Code Pro", Monaco, Menlo, Consolas, "Courier New", monospace; 503 | color: var(--text); 504 | font-weight: 500; 505 | display: inline-block; 506 | box-sizing: border-box; 507 | min-width: 100%; 508 | padding: 0.5em; 509 | margin-top: 0.5em; 510 | margin-bottom: 0.5em; 511 | font-size: 0.85em; 512 | white-space: pre !important; 513 | overflow-y: hidden; 514 | overflow-x: visible; 515 | background-color: var(--secondary-background); 516 | border: 1px solid var(--border); 517 | -webkit-border-radius: 6px; 518 | -moz-border-radius: 6px; 519 | border-radius: 6px; } 520 | 521 | .pre-scrollable { 522 | max-height: 340px; 523 | overflow-y: scroll; } 524 | 525 | 526 | /* Nim line-numbered tables */ 527 | .line-nums-table { 528 | width: 100%; 529 | table-layout: fixed; } 530 | 531 | table.line-nums-table { 532 | border-radius: 4px; 533 | border: 1px solid #cccccc; 534 | background-color: ghostwhite; 535 | border-collapse: separate; 536 | margin-top: 15px; 537 | margin-bottom: 25px; } 538 | 539 | .line-nums-table tbody { 540 | border: none; } 541 | 542 | .line-nums-table td pre { 543 | border: none; 544 | background-color: transparent; } 545 | 546 | .line-nums-table td.blob-line-nums { 547 | width: 28px; } 548 | 549 | .line-nums-table td.blob-line-nums pre { 550 | color: #b0b0b0; 551 | -webkit-filter: opacity(75%); 552 | text-align: right; 553 | border-color: transparent; 554 | background-color: transparent; 555 | padding-left: 0px; 556 | margin-left: 0px; 557 | padding-right: 0px; 558 | margin-right: 0px; } 559 | 560 | 561 | table { 562 | max-width: 100%; 563 | background-color: transparent; 564 | margin-top: 0.5em; 565 | margin-bottom: 1.5em; 566 | border-collapse: collapse; 567 | border-color: var(--third-background); 568 | border-spacing: 0; 569 | font-size: 0.9em; 570 | } 571 | 572 | table th, table td { 573 | padding: 0px 0.5em 0px; 574 | border-color: var(--third-background); 575 | } 576 | 577 | table th { 578 | background-color: var(--third-background); 579 | border-color: var(--third-background); 580 | font-weight: bold; } 581 | 582 | table th.docinfo-name { 583 | background-color: transparent; 584 | } 585 | 586 | table tr:hover { 587 | background-color: var(--third-background); } 588 | 589 | 590 | /* rst2html default used to remove borders from tables and images */ 591 | .borderless, table.borderless td, table.borderless th { 592 | border: 0; } 593 | 594 | table.borderless td, table.borderless th { 595 | /* Override padding for "table.docutils td" with "! important". 596 | The right padding separates the table cells. */ 597 | padding: 0 0.5em 0 0 !important; } 598 | 599 | .first { 600 | /* Override more specific margin styles with "! important". */ 601 | margin-top: 0 !important; } 602 | 603 | .last, .with-subtitle { 604 | margin-bottom: 0 !important; } 605 | 606 | .hidden { 607 | display: none; } 608 | 609 | blockquote.epigraph { 610 | margin: 2em 5em; } 611 | 612 | dl.docutils dd { 613 | margin-bottom: 0.5em; } 614 | 615 | object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] { 616 | overflow: hidden; } 617 | 618 | 619 | div.figure { 620 | margin-left: 2em; 621 | margin-right: 2em; } 622 | 623 | div.footer, div.header { 624 | clear: both; 625 | text-align: center; 626 | color: #666; 627 | font-size: smaller; } 628 | 629 | div.footer { 630 | padding-top: 5em; 631 | } 632 | 633 | div.line-block { 634 | display: block; 635 | margin-top: 1em; 636 | margin-bottom: 1em; } 637 | 638 | div.line-block div.line-block { 639 | margin-top: 0; 640 | margin-bottom: 0; 641 | margin-left: 1.5em; } 642 | 643 | div.topic { 644 | margin: 2em; } 645 | 646 | div.search_results { 647 | background-color: antiquewhite; 648 | margin: 3em; 649 | padding: 1em; 650 | border: 1px solid #4d4d4d; 651 | } 652 | 653 | div#global-links ul { 654 | margin-left: 0; 655 | list-style-type: none; 656 | } 657 | 658 | div#global-links > simple-boot { 659 | margin-left: 3em; 660 | } 661 | 662 | hr.docutils { 663 | width: 75%; } 664 | 665 | img.align-left, .figure.align-left, object.align-left { 666 | clear: left; 667 | float: left; 668 | margin-right: 1em; } 669 | 670 | img.align-right, .figure.align-right, object.align-right { 671 | clear: right; 672 | float: right; 673 | margin-left: 1em; } 674 | 675 | img.align-center, .figure.align-center, object.align-center { 676 | display: block; 677 | margin-left: auto; 678 | margin-right: auto; } 679 | 680 | .align-left { 681 | text-align: left; } 682 | 683 | .align-center { 684 | clear: both; 685 | text-align: center; } 686 | 687 | .align-right { 688 | text-align: right; } 689 | 690 | /* reset inner alignment in figures */ 691 | div.align-right { 692 | text-align: inherit; } 693 | 694 | p.attribution { 695 | text-align: right; 696 | margin-left: 50%; } 697 | 698 | p.caption { 699 | font-style: italic; } 700 | 701 | p.credits { 702 | font-style: italic; 703 | font-size: smaller; } 704 | 705 | p.label { 706 | white-space: nowrap; } 707 | 708 | p.rubric { 709 | font-weight: bold; 710 | font-size: larger; 711 | color: maroon; 712 | text-align: center; } 713 | 714 | p.topic-title { 715 | font-weight: bold; } 716 | 717 | pre.address { 718 | margin-bottom: 0; 719 | margin-top: 0; 720 | font: inherit; } 721 | 722 | pre.literal-block, pre.doctest-block, pre.math, pre.code { 723 | margin-left: 2em; 724 | margin-right: 2em; } 725 | 726 | pre.code .ln { 727 | color: grey; } 728 | 729 | /* line numbers */ 730 | pre.code, code { 731 | background-color: #eeeeee; } 732 | 733 | pre.code .comment, code .comment { 734 | color: #5c6576; } 735 | 736 | pre.code .keyword, code .keyword { 737 | color: #3B0D06; 738 | font-weight: bold; } 739 | 740 | pre.code .literal.string, code .literal.string { 741 | color: #0c5404; } 742 | 743 | pre.code .name.builtin, code .name.builtin { 744 | color: #352b84; } 745 | 746 | pre.code .deleted, code .deleted { 747 | background-color: #DEB0A1; } 748 | 749 | pre.code .inserted, code .inserted { 750 | background-color: #A3D289; } 751 | 752 | span.classifier { 753 | font-style: oblique; } 754 | 755 | span.classifier-delimiter { 756 | font-weight: bold; } 757 | 758 | span.option { 759 | white-space: nowrap; } 760 | 761 | span.problematic { 762 | color: #b30000; } 763 | 764 | span.section-subtitle { 765 | /* font-size relative to parent (h1..h6 element) */ 766 | font-size: 80%; } 767 | 768 | span.DecNumber { 769 | color: var(--number); } 770 | 771 | span.BinNumber { 772 | color: var(--number); } 773 | 774 | span.HexNumber { 775 | color: var(--number); } 776 | 777 | span.OctNumber { 778 | color: var(--number); } 779 | 780 | span.FloatNumber { 781 | color: var(--number); } 782 | 783 | span.Identifier { 784 | color: var(--identifier); } 785 | 786 | span.Keyword { 787 | font-weight: 600; 788 | color: var(--keyword); } 789 | 790 | span.StringLit { 791 | color: var(--literal); } 792 | 793 | span.LongStringLit { 794 | color: var(--literal); } 795 | 796 | span.CharLit { 797 | color: var(--literal); } 798 | 799 | span.EscapeSequence { 800 | color: var(--escapeSequence); } 801 | 802 | span.Operator { 803 | color: var(--operator); } 804 | 805 | span.Punctuation { 806 | color: var(--punctuation); } 807 | 808 | span.Comment, span.LongComment { 809 | font-style: italic; 810 | font-weight: 400; 811 | color: var(--comment); } 812 | 813 | span.RegularExpression { 814 | color: darkviolet; } 815 | 816 | span.TagStart { 817 | color: darkviolet; } 818 | 819 | span.TagEnd { 820 | color: darkviolet; } 821 | 822 | span.Key { 823 | color: #252dbe; } 824 | 825 | span.Value { 826 | color: #252dbe; } 827 | 828 | span.RawData { 829 | color: var(--raw-data); } 830 | 831 | span.Assembler { 832 | color: #252dbe; } 833 | 834 | span.Preprocessor { 835 | color: #252dbe; } 836 | 837 | span.Directive { 838 | color: #252dbe; } 839 | 840 | span.Command, span.Rule, span.Hyperlink, span.Label, span.Reference, 841 | span.Other { 842 | color: var(--other); } 843 | 844 | /* Pop type, const, proc, and iterator defs in nim def blocks */ 845 | dt pre > span.Identifier, dt pre > span.Operator { 846 | color: var(--identifier); 847 | font-weight: 700; } 848 | 849 | dt pre > span.Keyword ~ span.Identifier, dt pre > span.Identifier ~ span.Identifier, 850 | dt pre > span.Operator ~ span.Identifier, dt pre > span.Other ~ span.Identifier { 851 | color: var(--identifier); 852 | font-weight: inherit; } 853 | 854 | /* Nim sprite for the footer (taken from main page favicon) */ 855 | .nim-sprite { 856 | display: inline-block; 857 | width: 51px; 858 | height: 14px; 859 | background-position: 0 0; 860 | background-size: 51px 14px; 861 | -webkit-filter: opacity(50%); 862 | background-repeat: no-repeat; 863 | background-image: var(--nim-sprite-base64); 864 | margin-bottom: 5px; } 865 | 866 | span.pragmadots { 867 | /* Position: relative frees us up to make the dots 868 | look really nice without fucking up the layout and 869 | causing bulging in the parent container */ 870 | position: relative; 871 | /* 1px down looks slightly nicer */ 872 | top: 1px; 873 | padding: 2px; 874 | background-color: var(--third-background); 875 | border-radius: 4px; 876 | margin: 0 2px; 877 | cursor: pointer; 878 | font-size: 0.8em; 879 | } 880 | 881 | span.pragmadots:hover { 882 | background-color: var(--hint); 883 | } 884 | span.pragmawrap { 885 | display: none; 886 | } 887 | 888 | span.attachedType { 889 | display: none; 890 | visibility: hidden; 891 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------