├── .github └── workflows │ └── push.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── NOTICE.md ├── README.md ├── splitflap-doc ├── constructs.scrbl ├── info.rkt ├── js │ └── ticker-board.min.js ├── makefile ├── misc.rkt ├── mod-splitflap.scrbl ├── notes.scrbl ├── splitflap.scrbl ├── styles │ ├── flappy.css │ ├── flappy.tex │ ├── terminal.css │ └── terminal.tex └── tutorial.scrbl ├── splitflap-lib ├── constructs.rkt ├── info.rkt ├── main.rkt └── private │ ├── build.rkt │ ├── dust.rkt │ ├── entities.rktd │ ├── feed.rkt │ ├── mime-types.rktd │ ├── validation.rkt │ ├── version.rkt │ └── xml-generic.rkt ├── splitflap-tests ├── info.rkt └── tests │ ├── construct-tests.rkt │ ├── feed-tests.rkt │ └── util-tests.rkt └── splitflap └── info.rkt /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | types: [opened, synchronize] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | racket-variant: ['BC', 'CS'] 15 | racket-version: ['stable', 'current'] 16 | name: Test on Racket ${{ matrix.racket-version }} ${{ matrix.racket-variant }} 17 | steps: 18 | - name: Checkout splitflap repo 19 | uses: actions/checkout@master 20 | - name: Install Racket ${{ matrix.racket-version }} ${{ matrix.racket-variant }} 21 | uses: Bogdanp/setup-racket@v1.11 22 | with: 23 | architecture: 'x64' 24 | distribution: 'full' 25 | variant: ${{ matrix.racket-variant }} 26 | version: ${{ matrix.racket-version }} 27 | - name: Install splitflap lib and dependencies 28 | run: raco pkg install --auto --batch --link splitflap-lib/ 29 | - name: Run tests 30 | run: raco test splitflap-tests 31 | - name: Build docs 32 | run: raco pkg install --auto --batch --link splitflap-doc/ 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | \#* 3 | .\#* 4 | .DS_Store 5 | *.swp 6 | compiled/ 7 | doc/ 8 | *.html 9 | *.js 10 | !splitflap-doc/js/*.js 11 | *.css 12 | !splitflap-doc/styles/*.css 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our community a 7 | harassment-free experience for everyone, regardless of age, body size, visible or invisible 8 | disability, ethnicity, sex characteristics, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or 10 | sexual identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and 13 | healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our community include: 18 | 19 | * Demonstrating empathy and kindness toward other people 20 | * Being respectful of differing opinions, viewpoints, and experiences 21 | * Giving and gracefully accepting constructive feedback 22 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the 23 | experience 24 | * Focusing on what is best not just for us as individuals, but for the overall community 25 | 26 | Examples of unacceptable behavior include: 27 | 28 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 29 | * Trolling, insulting or derogatory comments, and personal or political attacks 30 | * Public or private harassment 31 | * Publishing others' private information, such as a physical or email address, without their 32 | explicit permission 33 | * Other conduct which could reasonably be considered inappropriate in a professional setting 34 | 35 | ## Enforcement Responsibilities 36 | 37 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior 38 | and will take appropriate and fair corrective action in response to any behavior that they deem 39 | inappropriate, threatening, offensive, or harmful. 40 | 41 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, 42 | code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and 43 | will communicate reasons for moderation decisions when appropriate. 44 | 45 | ## Scope 46 | 47 | This Code of Conduct applies within all community spaces, and also applies when an individual is 48 | officially representing the community in public spaces. Examples of representing our community 49 | include using an official e-mail address, posting via an official social media account, or acting as 50 | an appointed representative at an online or offline event. 51 | 52 | ## Enforcement 53 | 54 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community 55 | leaders responsible for enforcement at (= Joel Dueck). All complaints will be 56 | reviewed and investigated promptly and fairly. 57 | 58 | All community leaders are obligated to respect the privacy and security of the reporter of any 59 | incident. 60 | 61 | ## Enforcement Guidelines 62 | 63 | Community leaders will follow these Community Impact Guidelines in determining the consequences for 64 | any action they deem in violation of this Code of Conduct: 65 | 66 | ### 1. Correction 67 | 68 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or 69 | unwelcome in the community. 70 | 71 | **Consequence**: A private, written warning from community leaders, providing clarity around the 72 | nature of the violation and an explanation of why the behavior was inappropriate. A public apology 73 | may be requested. 74 | 75 | ### 2. Warning 76 | 77 | **Community Impact**: A violation through a single incident or series of actions. 78 | 79 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people 80 | involved, including unsolicited interaction with those enforcing the Code of Conduct, for a 81 | specified period of time. This includes avoiding interactions in community spaces as well as 82 | external channels like social media. Violating these terms may lead to a temporary or permanent ban. 83 | 84 | ### 3. Temporary Ban 85 | 86 | **Community Impact**: A serious violation of community standards, including sustained inappropriate 87 | behavior. 88 | 89 | **Consequence**: A temporary ban from any sort of interaction or public communication with the 90 | community for a specified period of time. No public or private interaction with the people involved, 91 | including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this 92 | period. Violating these terms may lead to a permanent ban. 93 | 94 | ### 4. Permanent Ban 95 | 96 | **Community Impact**: Demonstrating a pattern of violation of community standards, including 97 | sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement 98 | of classes of individuals. 99 | 100 | **Consequence**: A permanent ban from any sort of public interaction within the community. 101 | 102 | ## Attribution 103 | 104 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at 105 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 106 | 107 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla 108 | CoC]. 109 | 110 | For answers to common questions about this code of conduct, see the FAQ at 111 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 112 | [https://www.contributor-covenant.org/translations][translations]. 113 | 114 | [homepage]: https://www.contributor-covenant.org 115 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 116 | [Mozilla CoC]: https://github.com/mozilla/diversity 117 | [FAQ]: https://www.contributor-covenant.org/faq 118 | [translations]: https://www.contributor-covenant.org/translations 119 | 120 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Blue Oak Model License 2 | 3 | Version 1.0.0 4 | 5 | ## Purpose 6 | 7 | This license gives everyone as much permission to work with this software as possible, while 8 | protecting contributors from liability. 9 | 10 | ## Acceptance 11 | 12 | In order to receive this license, you must agree to its rules. The rules of this license are both 13 | obligations under that agreement and conditions to your license. You must not do anything with this 14 | software that triggers a rule that you cannot or will not follow. 15 | 16 | ## Copyright 17 | 18 | Each contributor licenses you to do everything with this software that would otherwise infringe that 19 | contributor's copyright in it. 20 | 21 | ## Notices 22 | 23 | You must ensure that everyone who gets a copy of any part of this software from you, with or without 24 | changes, also gets the text of this license or a link to . 25 | 26 | ## Excuse 27 | 28 | If anyone notifies you in writing that you have not complied with [Notices](#notices), you can keep 29 | your license by taking all practical steps to comply within 30 days after the notice. If you do not 30 | do so, your license ends immediately. 31 | 32 | ## Patent 33 | 34 | Each contributor licenses you to do everything with this software that would otherwise infringe any 35 | patent claims they can license or become able to license. 36 | 37 | ## Reliability 38 | 39 | No contributor can revoke this license. 40 | 41 | ## No Liability 42 | 43 | ***As far as the law allows, this software comes as is, without any warranty or condition, and no 44 | contributor will be liable to anyone for any damages related to this software or this license, under 45 | any kind of legal claim.*** 46 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | The file `splitflap-doc/js/ticker-board.min.js` is included from the [ticker-board][tb] project by 2 | Robin James Kerrison, under the terms of the MIT license. 3 | 4 | [tb]: https://github.com/rjkerrison/ticker-board 5 | 6 | MIT License 7 | 8 | Copyright (c) 2020 Robin James Kerrison 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `splitflap` 2 | 3 | [![CI](https://github.com/otherjoel/splitflap/actions/workflows/push.yml/badge.svg)](https://github.com/otherjoel/splitflap/actions) 4 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](CODE_OF_CONDUCT.md) 5 | 6 | 🔖⚛️ RSS / Atom feed generation library for Racket. 7 | 8 | Everything you supply is validated, so the result is always either a valid feed or an exception. 9 | 10 | **Documentation is at ** 11 | 12 | Splitflap requires Racket 8.1 or higher. To install Splitflap from the command line: 13 | 14 | > raco pkg install splitflap 15 | 16 | Or using DrRacket: click the **File** menu → **Install Package …** and enter `splitflap`. 17 | 18 | If deploying Splitflap in a production environment, you probably want to use `splitflap-lib` instead 19 | of `splitflap`. This will avoid fetching/building the docs, and will greatly reduce the number of 20 | dependencies installed. 21 | -------------------------------------------------------------------------------- /splitflap-doc/constructs.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/manual 2 | 3 | @(require "misc.rkt" 4 | scribble/examples 5 | (only-in scribble/eval interaction) 6 | (only-in scribble/bnf nonterm) 7 | (for-label (except-in gregor date date?) 8 | net/url 9 | racket/base 10 | racket/contract 11 | racket/file 12 | racket/promise 13 | racket/string 14 | splitflap 15 | txexpr)) 16 | 17 | @title[#:tag "mod-constructs"]{Feed Constructs} 18 | 19 | @(define mod-constructs (make-base-eval #:lang 'racket/base)) 20 | @(mod-constructs '(require splitflap gregor racket/file racket/promise)) 21 | 22 | @defmodule[splitflap/constructs #:use-sources (splitflap/private/validation splitflap)] 23 | 24 | The format of feeds is specified by the @Atom1.0[] and @RSS2.0[] specifications (and, for all 25 | practical purposes, by @AppleRequirements[] in the case of podcasts). These in turn reference other 26 | RFCs to specify the format of many individual elements: timestamps, domain names, email addresses, 27 | people, identifiers, and languages. 28 | 29 | Splitflap makes heavy use of custom @seclink["contract-boundaries" #:doc '(lib 30 | "scribblings/guide/guide.scrbl")]{contracts} to ensure conformity to the spec at every level. In 31 | cases where it makes things simpler, Splitflap is a bit @emph{more} strict than the actual spec. 32 | 33 | The bindings documented in this section are provided by the main @racketmodname[splitflap] module 34 | as well as by @racketmodname[splitflap/constructs]. 35 | 36 | @section{Tag URIs} 37 | 38 | Feeds, and items contained in feeds, require some globally unique identifier. Although any kind of 39 | reasonably unique identifier can be used in a feed, Splitflap takes the unreasonably opinionated 40 | stance of allowing only @tech{tag URIs}, which are easy to create and read, and which can remain 41 | stable even if the resource’s URL changes. 42 | 43 | A @deftech{tag URI} is an identifier of the form 44 | @racketvalfont{@litchar{tag:}@nonterm{authority}@litchar{,}@nonterm{date}@litchar{:}@nonterm{specific}}. 45 | The @nonterm{@deftech{authority}} is a domain name (or email address) held by you as of 46 | @nonterm{date}; together, the authority and the date form a unique @italic{tagging entity}, which 47 | acts kind of like a namespace. The @nonterm{@deftech{specific}} is a string uniquely identifying a 48 | particular resource (e.g., a page or file) within the tagging entity. 49 | 50 | The tag URI scheme is formalized in @hyperlink["https://datatracker.ietf.org/doc/html/rfc4151"]{RFC 51 | 4151}. 52 | 53 | @defproc[(mint-tag-uri [authority (or/c dns-domain? email-address?)] 54 | [date tag-entity-date?] 55 | [specific tag-specific-string?]) 56 | tag-uri?]{ 57 | 58 | Returns a @tech{tag URI} struct for use as a unique identifier in a @racket[feed-item], 59 | @racket[feed], @racket[@episode] or @racket[podcast]. 60 | 61 | The @racket[_date] must be any date on which you had ownership or assignment of the domain or 62 | email address at 00:00 UTC (the start of the day). (See @racket[tag-entity-date?].) 63 | 64 | The @racket[_specific] is a string that must be reliably and permanently unique within the set of 65 | things that your feed is serving. See @racket[tag-specific-string?] for information about what 66 | characters are allowed here. 67 | 68 | @examples[#:eval mod-constructs 69 | (mint-tag-uri "rclib.example.com" "2012-04-01" "Marian'sBlog") 70 | (mint-tag-uri "diveintomark.example.com" "2003" "3.2397")] 71 | 72 | } 73 | 74 | @defproc[(tag-uri->string [tag tag-uri?]) non-empty-string?]{ 75 | 76 | Converts a @racketlink[mint-tag-uri]{@tt{tag-uri}} into a string. 77 | 78 | @examples[#:eval mod-constructs 79 | (define rclib-id (mint-tag-uri "rclib.example.com" "2012-04-01" "Marian'sBlog")) 80 | (tag-uri->string rclib-id)] 81 | 82 | } 83 | 84 | @defproc[(append-specific [tag tag-uri?] [suffix tag-specific-string?]) tag-uri?]{ 85 | 86 | Returns a copy of @racket[_tag] with @racket[_suffix] appended to the @tech{specific} segment of 87 | the tag URI. This allows you to append to a feed’s @tech{tag URI} to create unique identifiers for 88 | the items within that feed. 89 | 90 | @examples[#:eval mod-constructs 91 | (define kottke-id (mint-tag-uri "kottke.example.com" "2005-12" "1")) 92 | kottke-id 93 | (append-specific kottke-id "post-slug")] 94 | 95 | } 96 | 97 | @defproc[(tag=? [tag1 tag-uri?] [tag2 tag-uri?]) boolean?]{ 98 | 99 | @margin-note{The tag URI spec 100 | @hyperlink["https://datatracker.ietf.org/doc/html/rfc4151#section-2.4"]{defines} tags as being equal 101 | when their byte-strings are indistinguishable.} 102 | 103 | Returns @racket[#t] if the @racket[tag-uri->string] representation of @racket[_tag1] and 104 | @racket[_tag2] are @racket[equal?], @racket[#f] otherwise. 105 | 106 | } 107 | 108 | @defproc[(tag-entity-date? [str string?]) boolean?]{ 109 | 110 | Returns @racket[#t] if @racket[_str] is a string of the form @racket{YYYY[-MM[-DD]]} --- that is, an 111 | acceptable date format for a @tech{tag URI} according to RFC 4151. 112 | 113 | @examples[#:eval mod-constructs 114 | @(code:comment @#,elem{Equivalent to January 1, 2012}) 115 | (tag-entity-date? "2012") 116 | @(code:comment @#,elem{Equivalent to June 1, 2012}) 117 | (tag-entity-date? "2012-06") 118 | @(code:comment @#,elem{take a guess on this one}) 119 | (tag-entity-date? "2012-10-21") 120 | (tag-entity-date? "2012-1-1")] 121 | } 122 | 123 | @defproc[(tag-specific-string? [str string?]) boolean?]{ 124 | 125 | Returns @racket[#t] if @racket[_str] is an acceptable string for the @tech{specific} portion of a 126 | @tech{tag URI} as specified in RFC 4151: a string comprised only of the characters in the range 127 | @litchar{a–z}, @litchar{A–Z}, @litchar{0–9} or in the set @litchar{-._~!$&'()*+,;=:@"@"/?}. 128 | 129 | @examples[#:eval mod-constructs 130 | (tag-specific-string? "abcdABCD01923") 131 | (tag-specific-string? "-._~!$&'()*+,;=:@/?") 132 | (tag-specific-string? "") 133 | (tag-specific-string? "^")] 134 | 135 | } 136 | 137 | @defproc[(normalize-tag-specific [str string?]) tag-specific-string?]{ 138 | 139 | Replaces any characters that would disqualify @racket[_str] from being a valid 140 | @racket[tag-specific-string?] with hyphens. Useful when you want to set the @tech{specific} portion 141 | of a @tech{tag URI} programmatically. 142 | 143 | @examples[#:eval mod-constructs 144 | (tag-specific-string? "my blog") 145 | (normalize-tag-specific "my-blog") 146 | (tag-specific-string? (normalize-tag-specific "my-blog")) 147 | 148 | (tag-specific-string? " tra^^shy\\` GarB§ºage ###") 149 | (normalize-tag-specific " tra^^shy\\` GarB§ºage ###") 150 | (tag-specific-string? (normalize-tag-specific " tra^^shy\\` GarB§ºage ###"))] 151 | 152 | } 153 | 154 | @defproc[(tag-uri? [v any/c]) boolean?]{ 155 | 156 | Returns @racket[#t] when @racket[_v] is a @racketlink[mint-tag-uri]{@tt{tag-uri}} struct. 157 | 158 | } 159 | 160 | @section{Persons} 161 | 162 | @defproc[(person [name non-empty-string?] 163 | [email email-address?] 164 | [url (or/c valid-url-string? #f) #f]) 165 | person?]{ 166 | 167 | Returns a @racketresultfont{#} struct for use in a @racket[feed-item], @racket[feed], 168 | @racket[episode] or @racket[podcast]. 169 | 170 | The @Atom1.0[] and @RSS2.0[] specs both have opinions about how people should be referenced in 171 | feeds. Atom requires only a name but also allows up to one email address and up to one URI. RSS 172 | requires one email address optionally followed by anything. So @racket[person] requires both a 173 | @racket[_name] and an @racket[_email], and the @racket[_url] is optional. 174 | 175 | } 176 | 177 | @defproc[(person->xexpr [p person?] [entity symbol?] [dialect (or/c 'rss 'atom 'itunes)]) txexpr?]{ 178 | 179 | Converts @racket[_p] into a tagged X-expresssion using @racket[_entity] as enclosing tag name. 180 | 181 | @examples[#:eval mod-constructs 182 | (define frank (person "Frankincense Pontipee" "frank@example.com")) 183 | (person->xexpr frank 'author 'atom) 184 | (person->xexpr frank 'contributor 'atom) 185 | (person->xexpr frank 'author 'rss) 186 | (person->xexpr frank 'itunes:owner 'itunes)] 187 | 188 | } 189 | 190 | @defproc[(person? [v any/c]) boolean?]{ 191 | 192 | Returns @racket[#t] when @racket[_v] is a @racket[person] struct, @racket[#f] otherwise. 193 | 194 | } 195 | 196 | @section{Date and time information} 197 | 198 | Feeds and feed items must be timestamped, and these values must include timezone information. 199 | Splitflap leans on the @racketmodname[gregor] library for this functionality --- in particular, 200 | @secref["moment" #:doc '(lib "gregor/scribblings/gregor.scrbl")] and @secref["timezone" #:doc '(lib 201 | "gregor/scribblings/gregor.scrbl")] --- and provides a couple of helper functions to make things a 202 | bit more ergonomic. 203 | 204 | @defproc[(infer-moment [str string? ""]) moment?]{ 205 | 206 | Parses from @racket[_str] and returns a precise @racket[moment], inferring time information where 207 | ommitted and using @racket[current-timezone] as the time zone for the moment. 208 | 209 | If @racket[_str] is @racketvalfont{""}, then the result of @racket[now/moment] is returned. 210 | Otherwise @racket[_str] must be in the form @racket{YYYY-MM-DD [hh:mm[:ss]]} or an exception is 211 | raised. If the seconds are ommitted, @racketvalfont{00} is assumed, and if the hours and minutes are 212 | ommitted, @racketvalfont{00:00:00} (the very start of the date) is assumed. 213 | 214 | @examples[#:eval mod-constructs 215 | (infer-moment "2012-08-31") 216 | (infer-moment "2012-08-31 13:34") 217 | (infer-moment "2015-10-02 01:03:15") 218 | 219 | (parameterize ([current-timezone -14400]) 220 | (infer-moment "2015-10-02 01:03:15")) 221 | 222 | (infer-moment "2012-09-14 12") 223 | (infer-moment)] 224 | 225 | @history[#:changed "1.2" "Added no-argument form for current moment"] 226 | 227 | } 228 | 229 | @defproc[(moment->string [m moment?] [dialect (or/c 'atom 'rss)]) non-empty-string?]{ 230 | 231 | Converts @racket[_m] into a timestamp in the format required by the chosen @racket[_dialect]: 232 | @hyperlink["https://datatracker.ietf.org/doc/html/rfc3339"]{RFC 3339} for Atom and 233 | @hyperlink["https://www.rfc-editor.org/rfc/rfc822.html"]{RFC 822} for RSS. 234 | 235 | @examples[#:eval mod-constructs 236 | (define m1 (infer-moment "2012-10-01")) 237 | (moment->string m1 'atom) 238 | (moment->string m1 'rss) 239 | (parameterize ([current-timezone 0]) 240 | (moment->string (infer-moment "2012-10-01") 'atom))] 241 | 242 | } 243 | 244 | @section{Enclosures and MIME types} 245 | 246 | An @deftech{enclosure} is an arbitrary resource related to a feed item that is potentially large in 247 | size and may require special handling. The canonical example is an MP3 file containing the audio for 248 | a podcast episode. 249 | 250 | @defstruct[enclosure ([url valid-url-string?] 251 | [mime-type (or/c non-empty-string? #f)] 252 | [size exact-nonnegative-integer?]) #:omit-constructor]{ 253 | 254 | A structure type for @tech{enclosures}. 255 | 256 | The @racket[_mime-type], if provided and not set to @racket[#f], must be a useable MIME type, but is 257 | not currently validated to ensure this. The @racket[_size] should be the resource’s size in bytes. 258 | 259 | This struct qualifies as @racketlink[food?]{food}, so it can be converted to XML with 260 | @racket[express-xml]. 261 | 262 | } 263 | 264 | @defproc[(file->enclosure [file path-string?] [base-url valid-url-string?]) enclosure?]{ 265 | 266 | Returns an @racket[enclosure] for @racket[_file], with a MIME type matching the file’s extension (if 267 | it can be determined). The enclosure’s URL is set to the @racket[_base-url] plus the filename 268 | portion of @racket[_file], and the length is set to the file’s actual length in bytes. 269 | 270 | This procedure accesses the filesystem; if @racket[_file] does not exist, an exception is raised. 271 | 272 | @examples[#:eval mod-constructs 273 | (code:comment @#,elem{Make a temporary file}) 274 | (define audio-file (make-temporary-file "audio-~a.m4a")) 275 | audio-file 276 | (display-to-file (make-bytes 100 66) audio-file #:exists 'truncate) 277 | (code:comment @#,elem{Pass the temp file to an enclosure}) 278 | (display 279 | (express-xml (file->enclosure audio-file "http://example.com") 'atom)) 280 | (code:comment @#,elem{Cleanup}) 281 | (delete-file audio-file)] 282 | } 283 | 284 | @defthing[mime-types-by-ext (promise/c (hash/c symbol? string?))]{ 285 | 286 | @margin-note{This table is built directly from 287 | @hyperlink["https://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?view=markup"]{the 288 | list maintained in the Apache SVN repository}.} 289 | 290 | A @tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{promise} that, when @racket[force]d, 291 | yields a hash table mapping file extensions (in lowercase symbol form) to MIME types. 292 | 293 | @examples[#:eval mod-constructs 294 | (hash-ref (force mime-types-by-ext) 'epub)] 295 | 296 | } 297 | 298 | @defproc[(path/string->mime-type [path path-string?]) (or/c string? #f)]{ 299 | 300 | Parses a file extension from @racket[_path] and returns its corresponding MIME type if one exists 301 | in @racket[mime-types-by-ext], @racket[#f] otherwise. This function does not access the file system. 302 | 303 | @examples[#:eval mod-constructs 304 | (path/string->mime-type ".m4a") 305 | (path/string->mime-type "SIGIL_v1_21.wad") 306 | (code:line (path/string->mime-type "mp3") (code:comment "No period, so no file extension!"))] 307 | 308 | } 309 | 310 | @section{Domains, URLs and email addresses} 311 | 312 | @defproc[(dns-domain? [v any/c]) boolean?]{ 313 | 314 | Returns @racket[#t] if @racket[_v] is a string whose entire contents are a valid DNS domain 315 | according to @hyperlink["https://datatracker.ietf.org/doc/html/rfc1035"]{RFC 1035}: 316 | 317 | @itemlist[ 318 | 319 | @item{Must contain one or more @emph{labels} separated by @litchar{.}} 320 | 321 | @item{Each label must consist of only the characters @litchar{A–Z}, @litchar{a–z}, @litchar{0–9}, or 322 | @litchar{-}.} 323 | 324 | @item{Labels may not start with a digit or a hyphen, and may not end in a hyphen.} 325 | 326 | @item{No individual label may be longer than 63 bytes (including an extra byte for a length header), 327 | and the entire domain may not be longer than 255 bytes.} 328 | 329 | ] 330 | 331 | @examples[#:eval mod-constructs 332 | (dns-domain? "a") 333 | (dns-domain? "rclib.org") 334 | (dns-domain? "a.b.c.d.e-f") 335 | (dns-domain? "a.b1000.com") 336 | code:blank 337 | (define longest-valid-label (make-string 62 #\a)) 338 | (define longest-valid-domain 339 | (string-append longest-valid-label "." (code:comment @#,elem{63 bytes (including length header)}) 340 | longest-valid-label "." (code:comment @#,elem{126}) 341 | longest-valid-label "." (code:comment @#,elem{189}) 342 | longest-valid-label "." (code:comment @#,elem{252}) 343 | "aa")) (code:comment @#,elem{255 bytes}) 344 | code:blank 345 | (dns-domain? longest-valid-label) 346 | (dns-domain? longest-valid-domain) 347 | (dns-domain? (string-append longest-valid-label "a")) 348 | (dns-domain? (string-append longest-valid-domain "a"))] 349 | } 350 | 351 | @defproc[(valid-url-string? [v any/c]) boolean?]{ 352 | 353 | Returns @racket[#t] if @racket[_v] is a “valid URL” for use in feeds. For this library’s purposes, a 354 | valid URL is one which, when parsed with @racket[string->url], includes a valid @tt{scheme} part 355 | (e.g. @racket{http://}), and in which the host is a @racket[dns-domain?] (and not, say, an IP 356 | address). 357 | 358 | @examples[#:eval mod-constructs 359 | (valid-url-string? "http://rclib.example.com") 360 | (valid-url-string? "telnet://rclib.example.com") 361 | (code:line (valid-url-string? "gonzo://example.com") (code:comment @#,elem{scheme need not be registered})) 362 | (code:line (valid-url-string? "https://user:p@example.com:8080") (code:comment @#,elem{includes user/password/port})) 363 | (code:line (valid-url-string? "file://C:\\home\\user?q=me") (code:comment @#,elem{Look, you do you})) 364 | code:blank 365 | (code:comment @#,elem{Valid URIs but not URLs:}) 366 | (code:line (valid-url-string? "news:comp.servers.unix") (code:comment @#,elem{no host given, only path})) 367 | (code:line (valid-url-string? "http://subdomain-.example.com") (code:comment @#,elem{invalid label})) 368 | code:blank 369 | (code:line (code:comment @#,elem{Valid URLs but not allowed by this library for use in feeds})) 370 | (code:line (valid-url-string? "ldap://[2001:db8::7]/c=GB?objectClass?one") (code:comment @#,elem{Host is not a DNS domain})) 371 | (code:line (valid-url-string? "telnet://192.0.2.16:80/") (code:comment @#,elem{ditto}))] 372 | 373 | } 374 | 375 | @defproc[(url-domain [u valid-url-string?]) dns-domain?]{ 376 | 377 | Returns only the domain (or “host”) portion of @racket[_u]. 378 | 379 | This function is convenient for @racket[mint-tag-uri] (which needs a valid @racket[dns-domain?]) 380 | when you already have a base URL for the site. 381 | 382 | @examples[#:eval mod-constructs 383 | (url-domain "http://example.com") 384 | (url-domain "https://user:p@example.com:8080/path/to/file")] 385 | 386 | } 387 | 388 | @defproc[(url-join [base valid-url-string?] [rel relative-path?]) valid-url-string?]{ 389 | 390 | Combines @racket[_base] with @racket[_rel] to form a new URL, ensuring the result is correctly 391 | encoded. On Windows machines, backslashes are converted to forward slashes. 392 | 393 | This is a convenient front-end to Racket’s @racket[combine-url/relative] and 394 | @racket[relative-path->relative-url-string]. 395 | 396 | @examples[#:eval mod-constructs 397 | (url-join "http://example.com" "path/to/my file.html") 398 | (url-join "http://example.com/" "path/to/resource") 399 | (eval:error (url-join "http://example.com" "/absolute/path"))] 400 | 401 | } 402 | 403 | 404 | @defproc[(email-address? [v any/c]) boolean?]{ 405 | 406 | Returns @racket[#t] if @racket[_v] is a valid email address according to what is essentially a 407 | common-sense subset of RFC 5322: 408 | 409 | @itemlist[ 410 | 411 | @item{Must be in the format @racketvalfont{@nonterm{local-part}@litchar{@"@"}@nonterm{domain}}} 412 | 413 | @item{The @nonterm{local-part} must be no longer than 65 bytes and only include @litchar{a–z}, 414 | @litchar{A–Z}, @litchar{0–9}, or characters in the set @litchar|{!#$%&'*+/=?^_‘{|}~-.}|.} 415 | 416 | @item{The @nonterm{domain} must be valid according to @racket[dns-domain?].} 417 | 418 | @item{The entire email address must be no longer than 255 bytes.} 419 | 420 | ] 421 | 422 | @examples[#:eval mod-constructs 423 | (email-address? "test-email.with+symbol@example.com") 424 | (email-address? "#!$%&'*+-/=?^_{}|~@example.com") 425 | code:blank 426 | (code:comment @#,elem{See also dns-domain? which applies to everything after the @"@" sign}) 427 | (email-address? "email@123.123.123.123") 428 | (email-address? "λ@example.com")] 429 | 430 | } 431 | 432 | @defproc[(validate-email-address [addr string?]) boolean?]{ 433 | 434 | Returns @racket[_addr] if it is a valid email address (according to the same rules as for 435 | @racket[email-address?]); otherwise, an exception is raised whose message explains the reason the 436 | address is invalid. 437 | 438 | @interaction[#:eval mod-constructs 439 | (validate-email-address "marian@rclib.example.com") 440 | (validate-email-address "@") 441 | (validate-email-address "me@myself@example.com") 442 | (validate-email-address ".marian@rclib.example.com") 443 | (validate-email-address "λ@example.com") 444 | (validate-email-address "lambda@1.example.com")] 445 | 446 | } 447 | 448 | @section{Language codes} 449 | 450 | @defthing[system-language (promise/c iso-639-language-code?)]{ 451 | 452 | A @tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{promise} that, when @racket[force]d, 453 | yields a two-letter symbol corresponding to the default language in use for the current user 454 | account/system. On Unix and Mac OS, the first two characters of the value returned by 455 | @racket[system-language+country] are used. On Windows, the first two characters of the value in the 456 | registry key @tt{HKEY_CURRENT_USER\Control Panel\International\LocaleName} are used. If the system 457 | language cannot be determined, an exception is raised the first time the promise is forced. 458 | 459 | @examples[#:eval mod-constructs 460 | (force system-language)] 461 | 462 | } 463 | 464 | @defproc[(iso-639-language-code? [v any/c]) boolean?]{ 465 | 466 | Returns @racket[#t] if @racket[_v] is a two-character lowercase symbol matching a two-letter 467 | @hyperlink["https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes"]{ISO639-1 language code}. 468 | 469 | @examples[#:eval mod-constructs 470 | (iso-639-language-code? 'fr) 471 | (iso-639-language-code? 'FR)] 472 | 473 | } 474 | 475 | @defthing[language-codes (listof iso-639-language-code?)]{ 476 | 477 | A list of symbols that qualify as @racket[iso-639-language-code?]. 478 | 479 | } 480 | -------------------------------------------------------------------------------- /splitflap-doc/info.rkt: -------------------------------------------------------------------------------- 1 | #lang info 2 | 3 | (define collection "splitflap") 4 | (define scribblings '(("splitflap.scrbl" (multi-page)))) 5 | 6 | (define deps '("scribble-lib" 7 | "base")) 8 | (define build-deps '("at-exp-lib" 9 | "net-doc" 10 | "txexpr" 11 | "gregor-doc" 12 | "gregor-lib" 13 | "racket-doc" 14 | "scribble-lib" 15 | "splitflap-lib")) 16 | 17 | (define update-implies '("splitflap-lib")) 18 | (define compile-omit-paths '("js" "styles")) 19 | (define pkg-desc "documentation part of \"splitflap\"") 20 | (define license 'BlueOak-1.0.0) 21 | -------------------------------------------------------------------------------- /splitflap-doc/js/ticker-board.min.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e={438:(e,t,n)=>{n.d(t,{Z:()=>h});var i=n(645),s=n.n(i),a=n(667),r=n.n(a),o=n(679),c=n(179),l=s()((function(e){return e[1]}));l.push([e.id,"@import url(https://fonts.googleapis.com/css2?family=Nanum+Gothic+Coding&family=Roboto+Mono:wght@100;400&display=swap);"]);var d=r()(o.Z),g=r()(c.Z);l.push([e.id,"ul.board {\n margin: 0;\n padding: 0;\n list-style: none;\n font-family: 'Nanum Gothic Coding', 'Roboto Mono', monospace;\n overflow-x: hidden;\n /* static for old browsers */\n font-size: 3rem;\n /* scale for new browsers */\n font-size: calc(3 * var(--base-size));\n max-width: min-content;\n --base-size: min(1rem, 2.5vw);\n}\n\n@media screen and (max-width: 600px) {\n ul.board {\n overflow-x: scroll;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\nul.board li div.ticker-row {\n display: flex;\n padding: calc(var(--base-size) / 4);\n}\n\nspan.ticker {\n display: block;\n background-size: cover;\n background-position: center left;\n background-repeat: no-repeat;\n overflow-y: hidden;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n /* static for old browsers */\n border-radius: 0.125rem;\n padding: 0.25rem 0;\n /* margin: 0 0.125rem; */\n /* scale for modern browsers */\n border-radius: calc(var(--base-size) / 8);\n padding: calc(var(--base-size) / 4) calc(var(--base-size) / 2);\n}\n\nspan.ticker.animating {\n animation: squeeze 0.075s ease-in-out infinite;\n}\n@keyframes squeeze {\n 50% {\n transform: scaleY(0);\n }\n}\n\nspan.ticker,\n[dark] span.ticker {\n color: hsl(60, 50%, 65%);\n background-image: url("+d+");\n}\n\n[light] span.ticker {\n color: hsl(240, 25%, 15%);\n background-image: url("+g+");\n}\n\n@media (prefers-color-scheme: light) {\n span.ticker {\n color: hsl(240, 25%, 15%);\n background-image: url("+g+");\n }\n}\n",""]);const h=l},645:e=>{e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n=e(t);return t[2]?"@media ".concat(t[2]," {").concat(n,"}"):n})).join("")},t.i=function(e,n,i){"string"==typeof e&&(e=[[null,e,""]]);var s={};if(i)for(var a=0;a{e.exports=function(e,t){return t||(t={}),"string"!=typeof(e=e&&e.__esModule?e.default:e)?e:(/^['"].*['"]$/.test(e)&&(e=e.slice(1,-1)),t.hash&&(e+=t.hash),/["'() \t\n]/.test(e)||t.needQuotes?'"'.concat(e.replace(/"/g,'\\"').replace(/\n/g,"\\n"),'"'):e)}},379:(e,t,n)=>{var i,s=function(){var e={};return function(t){if(void 0===e[t]){var n=document.querySelector(t);if(window.HTMLIFrameElement&&n instanceof window.HTMLIFrameElement)try{n=n.contentDocument.head}catch(e){n=null}e[t]=n}return e[t]}}(),a=[];function r(e){for(var t=-1,n=0;n{n.d(t,{Z:()=>i});const i="data:image/svg+xml;base64,PHN2ZwogIHZpZXdCb3g9IjAgMCAxMDAgMTUwIgogIHdpZHRoPSIxMHB4IgogIGhlaWdodD0iMjBweCIKICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgaWQ9InRpY2tlci1iYWNrZ3JvdW5kIgo+CiAgPHJlY3QKICAgIHg9IjEwIgogICAgeT0iNiIKICAgIHdpZHRoPSI4MCIKICAgIGhlaWdodD0iMTM4IgogICAgc3Ryb2tlPSJyZ2IoMTkzLCAxOTMsIDE5MykiCiAgICBzdHJva2Utd2lkdGg9IjMiCiAgICByeD0iNCIKICAgIGZpbGw9InJnYigyNDYsIDI0MywgMjI0KSIKICAvPgogIDxyZWN0IHg9IjE1IiB5PSI3NCIgd2lkdGg9IjcwIiBoZWlnaHQ9IjIiIGZpbGw9InJnYigxMjgsIDEyOCwgMTI4KSIgLz4KICA8Y2lyY2xlCiAgICBjeD0iMTAiCiAgICBjeT0iNzUiCiAgICByPSI0IgogICAgZmlsbD0icmdiKDE5MywgMTkzLCAxOTMpIgogICAgc3Ryb2tlPSJyZ2IoMjQ2LCAyNDMsIDIyNCkiCiAgICBzdHJva2Utd2lkdGg9IjMiCiAgLz4KICA8Y2lyY2xlCiAgICBjeD0iOTAiCiAgICBjeT0iNzUiCiAgICByPSI0IgogICAgZmlsbD0icmdiKDE5MywgMTkzLCAxOTMpIgogICAgc3Ryb2tlPSJyZ2IoMjQ2LCAyNDMsIDIyNCkiCiAgICBzdHJva2Utd2lkdGg9IjMiCiAgLz4KPC9zdmc+"},679:(e,t,n)=>{n.d(t,{Z:()=>i});const i="data:image/svg+xml;base64,PHN2ZwogIHZpZXdCb3g9IjAgMCAxMDAgMTUwIgogIHdpZHRoPSIxMHB4IgogIGhlaWdodD0iMjBweCIKICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgaWQ9InRpY2tlci1iYWNrZ3JvdW5kIgo+CiAgPHJlY3QKICAgIHg9IjEwIgogICAgeT0iNiIKICAgIHdpZHRoPSI4MCIKICAgIGhlaWdodD0iMTM4IgogICAgc3Ryb2tlPSJyZ2IoMTkzLCAxOTMsIDE5MykiCiAgICBzdHJva2Utd2lkdGg9IjMiCiAgICByeD0iNCIKICAgIGZpbGw9ImhzbCgyMTUsIDE1JSwgNSUpIgogIC8+CiAgPHJlY3QgeD0iMTUiIHk9Ijc0IiB3aWR0aD0iNzAiIGhlaWdodD0iMiIgZmlsbD0icmdiKDEyOCwgMTI4LCAxMjgpIiAvPgogIDxjaXJjbGUKICAgIGN4PSIxMCIKICAgIGN5PSI3NSIKICAgIHI9IjQiCiAgICBmaWxsPSJyZ2IoMTkzLCAxOTMsIDE5MykiCiAgICBzdHJva2U9Im5vbmUiCiAgICBzdHJva2Utd2lkdGg9IjIiCiAgLz4KICA8Y2lyY2xlCiAgICBjeD0iOTAiCiAgICBjeT0iNzUiCiAgICByPSI0IgogICAgZmlsbD0icmdiKDE5MywgMTkzLCAxOTMpIgogICAgc3Ryb2tlPSJub25lIgogICAgc3Ryb2tlLXdpZHRoPSIyIgogIC8+Cjwvc3ZnPg=="}},t={};function n(i){var s=t[i];if(void 0!==s)return s.exports;var a=t[i]={id:i,exports:{}};return e[i](a,a.exports,n),a.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var i in t)n.o(t,i)&&!n.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{const e=" ",t=class{constructor(t,{count:n,size:i,delay:s,theme:a}){this.options={delay:s||250,theme:a},this.messages=new Array(n).fill("".padEnd(i,e)),this._createElement(t),this._createTickers(i),this.update()}_createElement(e){const t=document.createElement("ul");if(t.classList.add("board"),"string"==typeof e&&(e=document.querySelector(e)),e instanceof HTMLElement&&e.replaceWith(t),"string"==typeof this.options.theme){const{theme:e}=this.options;console.log({theme:e,boardElement:t}),t.setAttribute(e,!0)}this.element=t}_createTickers(e){this.tickers=this.messages.map(((t,n)=>this.setupTicker(e,n)))}setupTicker(t,n){const i=new class{constructor(t,n){this.size=t,this.message=n.padEnd(t,e),this.delay=200,this.cards=this.createCards(),this.element=this.render()}updateMessage(t){this.message=t.padEnd(this.size,e),this.changeMessage()}changeMessage(){let t=this.message.replace(/\s/g,e);this.cards.forEach(((e,n)=>{setTimeout((()=>e.animateTo(t[n])),n*this.delay)})),this.screenReaderElement.innerText=this.message}createCards(){return new Array(this.size).fill("").map((()=>new class{acceptableCodes="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:,'- ";delayInMilliseconds=25;constructor(e){this.visibleLetter=e,this.createElement()}createElement(){this.element=document.createElement("span"),this.element.classList.add("ticker"),this.element.innerText=this.visibleLetter}changeCharacter(e){this.visibleLetter=e,this.render()}animateTo(e){if(this.visibleLetter===e)return;const t=(n=this.acceptableCodes,i=this.visibleLetter,s=e,function(e,t){return e.slice(0,e.indexOf(t)+1)}(function(e,t){return e.slice(e.indexOf(t)+1)}(n+n,i),s));var n,i,s;let a;const r=n=>{void 0===a&&(a=n);const i=n-a,s=Math.floor(i/this.delayInMilliseconds);s{t.appendChild(e.element)})),e.append(this.screenReaderElement,t),e}}(t,this.messages[n].padEnd(t,e));return this.element.appendChild(i.element),i}update(){this.tickers.forEach(((t,n)=>{this.messages[n]=(this.messages[n]||"").padEnd(this.size,e),setTimeout((()=>t.updateMessage(this.messages[n])),n*this.options.delay)}))}updateMessages(e){var t,n;this.messages=(t=e,"",(n=this.tickers.length)<=t.length?t:t.concat(new Array(n-t.length).fill(""))),this.update()}},i=class extends t{constructor(e,t){super(e,t),this.defaultMessage=new Array(t.size).fill(" ").join(""),this.originalMessages=t.messages,this.delay=t.delay||8e3,this.initialDelay=t.initialDelay||1e3,this.rotate()}rotate(){setTimeout((()=>{this.advance(),this.interval=setInterval(this.advance.bind(this),this.delay)}),this.initialDelay)}cancel(){clearInterval(this.interval)}advance(){const e=Array.from(this.messages);e.shift();const t=this.messages[this.messages.length-1],n=this.originalMessages.indexOf(t)+1;e.push(this.originalMessages[n]),this.updateMessages(e)}};var s=n(379),a=n.n(s),r=n(438);a()(r.Z,{insert:"head",singleton:!1}),r.Z.locals,window&&(window.Board=t,window.TickerBoard=class{constructor(e,t){const{size:n,theme:s,delay:a}=t||{},r=document.querySelectorAll(e);this.boards=Array.from(r).map((e=>{const t=Array.from(e.children).map((e=>e.innerText)),r=document.createElement("ul");e.replaceWith(r);const o=new i(r,{count:t.length,size:n||Math.max(...t.map((e=>e.length))),messages:t,theme:s,delay:a});return o.element.addEventListener("click",(()=>o.advance())),o}))}advance(e){this.boards[e].advance()}},window.RotationBoard=i)})()})(); -------------------------------------------------------------------------------- /splitflap-doc/makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | scribble: splitflap.scrbl 4 | scribble: ## Rebuild Scribble docs 5 | rm -rf splitflap/* || true 6 | scribble --htmls +m --redirect https://docs.racket-lang.org/local-redirect/ splitflap.scrbl 7 | 8 | publish: ## Sync Scribble HTML docs to web server (doesn’t rebuild anything) 9 | rsync -av --delete splitflap/ $(JDCOM_SRV)what-about/splitflap/ 10 | 11 | # Self-documenting makefile (http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html) 12 | help: ## Displays this help screen 13 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' 14 | 15 | .PHONY: help publish 16 | 17 | .DEFAULT_GOAL := help 18 | -------------------------------------------------------------------------------- /splitflap-doc/misc.rkt: -------------------------------------------------------------------------------- 1 | #lang at-exp racket/base 2 | 3 | (require racket/runtime-path 4 | scribble/core 5 | scribble/html-properties 6 | scribble/latex-properties 7 | scribble/manual) 8 | 9 | (provide (all-defined-out)) 10 | 11 | (define (Atom1.0 . x) 12 | (apply hyperlink "https://datatracker.ietf.org/doc/html/rfc4287" (if (null? x) '("Atom 1.0") x))) 13 | 14 | (define (RSS2.0 . x) 15 | (apply hyperlink "https://www.rssboard.org/rss-specification" (if (null? x) '("RSS 2.0") x))) 16 | 17 | (define (AppleRequirements . x) 18 | (apply hyperlink "https://podcasters.apple.com/support/823-podcast-requirements" 19 | (if (null? x) '("Apple’s Podcast feed requirements") x))) 20 | 21 | (define (W3CFeedValidator . x) 22 | (apply hyperlink "https://validator.w3.org/feed/" (if (null? x) '("W3C Feed Validator") x))) 23 | 24 | (define-runtime-path aux-css "styles/terminal.css") 25 | (define-runtime-path aux-tex "styles/terminal.tex") 26 | (define-runtime-path flappy-css "styles/flappy.css") 27 | (define-runtime-path flappy-tex "styles/flappy.tex") 28 | 29 | (define (terminal . args) 30 | (compound-paragraph (style "terminal" (list (color-property (list #x66 #x33 #x99)) 31 | (css-style-addition aux-css) 32 | (alt-tag "div") 33 | (tex-addition aux-tex))) 34 | (list (apply verbatim args)))) 35 | 36 | ;; Simulate a command-line prompt 37 | (define (:> . args) 38 | (element (style "prompt" (list (color-property (list #x66 #x66 #x66)))) 39 | (apply exec (cons "> " args)))) 40 | 41 | ;; Simulate a bash-style comment 42 | (define (rem . args) 43 | (apply racketcommentfont (cons "# " args))) 44 | 45 | (define (spec . args) 46 | (element (style "special" (list (color-property (list #x66 #x33 #x99)))) args)) 47 | 48 | (define-runtime-path flipboard-js "js/ticker-board.min.js") 49 | 50 | (define flipboard-div 51 | (paragraph 52 | (style "flipboard" 53 | (list 'div 54 | (css-style-addition flappy-css) 55 | (js-addition flipboard-js) 56 | (tex-addition flappy-tex) 57 | (attributes '((id . "flappy"))))) 58 | (list (elem "")))) 59 | 60 | (define flipboard-script 61 | @(paragraph 62 | (style "flip-js" (list (alt-tag "script"))) 63 | (list @literal|{ 64 | new RotationBoard(document.getElementById('flappy'), { 65 | messages: ['splitflap', 'XML Feeds'], 66 | count: 1, 67 | size: 9, 68 | delay: 6000, 69 | }) 70 | }|))) 71 | -------------------------------------------------------------------------------- /splitflap-doc/mod-splitflap.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/manual 2 | 3 | @(require 4 | "misc.rkt" 5 | scribble/examples 6 | (for-label splitflap 7 | (except-in gregor date? date) 8 | (only-in net/url url?) 9 | racket/base 10 | racket/promise 11 | (only-in racket/string non-empty-string?) 12 | txexpr 13 | xml) 14 | (for-syntax racket/base racket/syntax)) 15 | 16 | @title[#:tag "mod-splitflapp"]{Library Reference} 17 | 18 | @(define mod-feed (make-base-eval #:lang 'racket/base)) 19 | @(mod-feed '(require splitflap)) 20 | 21 | @section{Building feeds} 22 | 23 | Use @racket[feed-item] and @racket[feed] to create feeds for web content like blog posts, comments, 24 | or even notifications: any content with a timestamp and its own URL. 25 | 26 | You have a choice of using RSS or Atom formats, or both. Twenty years ago, holy wars were fought 27 | over which format was superior and it was necessary to supply both in order to assure compatibility 28 | with the most clients. These days almost every client supports both, so you probably only need to 29 | supply one. 30 | 31 | You should run all your feeds through the @W3CFeedValidator[]. Please 32 | @hyperlink["https://github.com/otherjoel/splitflap/issues"]{file an issue} should you 33 | encounter any validation errors in feeds created with Splitflap. 34 | 35 | 36 | @defproc[(feed-item [id tag-uri?] 37 | [url valid-url-string?] 38 | [title string?] 39 | [author person?] 40 | [published moment?] 41 | [updated moment?] 42 | [content xexpr?] 43 | [media (or/c enclosure? #f) #f]) 44 | feed-item?]{ 45 | 46 | Returns a @racketresultfont{#} struct for inclusion in a @racket[feed]. You can inspect 47 | its contents with @racket[express-xml]. 48 | 49 | The @racket[_id] argument must be a @tech{tag URI} (obtain from @racket[mint-tag-uri] or 50 | @racket[append-specific]). 51 | 52 | The @racket[_title] should not contain HTML markup; if it does, it will be escaped, and the raw markup may 53 | by shown in applications that use your feed. 54 | 55 | The @racket[_author] argument must be a @racket[person] struct. 56 | 57 | The value of @racket[_updated] must be identical to or after @racket[_published], taking time zone 58 | information into account, or an exception is raised. The values for these arguments can be most 59 | conveniently supplied by @racket[infer-moment], but any moment-producing method will work, such as 60 | constructing @racket[moment]s directly, parsing strings with @racket[parse-moment], etc. 61 | 62 | If @racket[_content] is a @racketlink[txexpr?]{tagged X-expression}, it will be included as 63 | XML-appropriate escaped HTML; if it is a plain string, it will be included as CDATA. 64 | 65 | You can optionally use the @racket[_media] argument to supply an @tech{enclosure}, but if you are 66 | generating a feed for a podcast you should consider using @racket[episode] and @racket[podcast] 67 | instead. 68 | 69 | } 70 | 71 | @defproc[(feed [id tag-uri?] 72 | [site-url valid-url-string?] 73 | [name string?] 74 | [entries (listof feed-item?)]) 75 | feed?]{ 76 | 77 | Returns a @racketresultfont{#} struct. You can inspect its contents with @racket[express-xml]. 78 | 79 | If any of the @tech{tag URIs} of the @racket[_entries] are @racket[tag=?] with each other or with the 80 | feed @racket[_id], an exception is raised identifying the first duplicate encountered. 81 | 82 | } 83 | 84 | @section{Producing feed XML} 85 | 86 | @defproc[(express-xml [data food?] 87 | [dialect (or/c 'rss 'atom)] 88 | [feed-url (or/c valid-url-string? #f) #f] 89 | [#:as result-type (or/c 'xml-string 'xexpr 'xml) 'xml-string]) 90 | (or/c string? txexpr? document? element?)]{ 91 | 92 | Returns the expression of @racket[_data] in one of three forms, depending on @racket[_result-type]: 93 | a string of XML, a @racketlink[txexpr?]{tagged X-expression}, or an XML object. In the latter case, 94 | the result is a @racket[document] when @racket[_data] is a @racket[feed] or a @racket[podcast], and 95 | an @racket[element] otherwise. 96 | 97 | The @racket[_dialect] argument is ignored when @racket[_data] is an @racket[episode] or a 98 | @racket[podcast], since @AppleRequirements[] stipulate the use of RSS 2.0 for podcast feeds. 99 | 100 | The @racket[_feed-url] argument must be supplied as a valid URL string when @racket[_data] is a 101 | @racket[feed] or a @racket[podcast]; this should be the URL where the feed itself will be located. 102 | It is not a required argument when @racket[_data] is any other type, and in those cases it will be 103 | ignored if supplied. 104 | 105 | For complete feeds (e.g., when @racket[_data] is a @racket[feed] or @racket[podcast]), the 106 | entries/episodes will be sorted in reverse chronological order by their “updated” timestamps, and 107 | the most recent such timestamp is used as the value for feed-level “last updated” and/or “published” 108 | elements. If the feed contains no entries or episodes, these feed-level timestamps will use 109 | @racket[now/moment]. 110 | 111 | The output of complete feeds can be further affected by other parameters (view their documentation 112 | for more information): 113 | 114 | @itemlist[#:style 'compact 115 | 116 | @item{If @racket[include-generator?] is @racket[#t], a @tt{generator} element will be included.} 117 | 118 | @item{If @racket[feed-language] is not @racket[#f], its value will be used as the language code for 119 | the feed; otherwise the result of @racket[(force system-language)] is used.} 120 | 121 | @item{If @racket[feed-xslt-stylesheet] is not @racket[#f], its value will be used as the path to a 122 | stylesheet for the feed.} 123 | 124 | ] 125 | 126 | @examples[#:eval mod-feed 127 | (define item 128 | (feed-item (mint-tag-uri "rclib.example.com" "2012-06" "blog:example-post") 129 | "http://rclib.example.com" 130 | "Example" 131 | (person "Marion Paroo" "marion@rclib.example.com") 132 | (infer-moment "2013-04-13 08:45") 133 | (infer-moment "2013-04-14") 134 | '(article (p "Etc…")))) 135 | (display (express-xml item 'atom #f)) 136 | (express-xml item 'atom #:as 'xexpr) 137 | 138 | ] 139 | 140 | } 141 | 142 | @defparam[include-generator? incl? boolean? #:value #t]{ 143 | 144 | When set to @racket[#t], @racket[express-xml] will include a @tt{generator} element in the feed 145 | generated for a @racket[feed] or @racket[podcast], naming @tt{Racket vN.n [cs/3m] / splitflap vN.n} 146 | as the generator of the feed. 147 | 148 | } 149 | 150 | @defparam[feed-language lang (or/c iso-639-language-code? #f) #:value #f]{ 151 | 152 | A parameter that, when not set to @racket[#f], is used by @racket[express-xml] as the language for a 153 | @racket[feed] or @racket[podcast] in place of @racket[system-language]. 154 | 155 | } 156 | 157 | @defparam[feed-xslt-stylesheet path (or/c url? non-empty-string? #f) #:value #f]{ 158 | 159 | If this parameter is not @racket[#f], @racket[express-xml] will use its value as the path to an 160 | @link["https://en.wikipedia.org/wiki/XSLT"]{XSLT stylesheet} in the prolog of the XML document 161 | produced for a @racket[feed] or @racket[podcast]. 162 | 163 | Generally, the XSLT file must be served from the same domain as the feed itself, otherwise the 164 | styling will not be applied. 165 | 166 | @margin-note{Stylesheets can solve the problem of feeds looking and behaving in unfriendly ways when 167 | accessed directly in a web browser. See 168 | @link["https://github.com/genmon/aboutfeeds/issues/8#issuecomment-673293655"]{this Github issue} for 169 | examples of people who have used XSLT stylesheets to add informative links and a welcoming layout to 170 | their feeds.} 171 | 172 | } 173 | 174 | @section{Podcasts} 175 | 176 | Splitflap provides some special data types for podcast feeds: @racket[episode] and 177 | @racket[podcast]. These are patterned after @AppleRequirements[] since those serve as a kind of 178 | de facto standard for this application. 179 | 180 | @defproc[(episode [id tag-uri?] 181 | [url valid-url-string?] 182 | [title string?] 183 | [author person?] 184 | [published moment?] 185 | [updated moment?] 186 | [content xexpr?] 187 | [media enclosure?] 188 | [#:duration duration (or/c exact-nonnegative-integer? #f) #f] 189 | [#:image-url image-url (or/c valid-url-string? #f) #f] 190 | [#:explicit? explicit any/c null] 191 | [#:episode-num ep-num (or/c exact-nonnegative-integer? #f) #f] 192 | [#:season-num s-num (or/c exact-nonnegative-integer? #f) #f] 193 | [#:type type (or/c 'trailer 'full 'bonus #f) #f] 194 | [#:block? block any/c #f]) 195 | episode?]{ 196 | 197 | Returns an @racketresultfont{#} struct, which is required for @racket[podcast]s in the same 198 | way that @racket[feed-item]s are required for @racket[feed]s. You can inspect its contents with 199 | @racket[express-xml]. 200 | 201 | The value of @racket[_updated] must be identical to or after @racket[_published], taking time zone 202 | information into account, or an exception is raised. The values for these arguments can be most 203 | conveniently supplied by @racket[infer-moment], but any moment-producing method will work, such as 204 | constructing @racket[moment]s directly, parsing strings with @racket[parse-moment], etc. 205 | 206 | If @racket[_content] is a @racketlink[txexpr?]{tagged X-expression}, it will be included as escaped 207 | HTML; if it is a plain string, it will be included as CDATA. 208 | 209 | Below are further notes about particular elements supplied to @racket[episode]. The @spec{colored 210 | passages} indicate things which are required by Apple for inclusion in the Apple Podcasts directory 211 | but which are @emph{not} validated by Splitflap. (See @AppleRequirements[].) 212 | 213 | @itemlist[ 214 | 215 | @item{The @racket[_title] should contain no markup, and @spec{should not contain episode or season 216 | number information (use @racket[#:episode-num] and @racket[#:season-num] for this instead)}.} 217 | 218 | @item{The @racket[#:image-url] is for episode-specific artwork. @spec{It must point to an image with 219 | a minimum size of 1400 ⨉ 1400 pixels and a maximum size of 3000 ⨉ 3000 pixels (the preferred size), 220 | in JPEG or PNG format, 72 dpi, with appropriate file extensions (.jpg, .png), and in the RGB 221 | colorspace.} See @hyperlink["https://podcasters.apple.com/support/896-artwork-requirements"]{Apple 222 | artwork requirements} for other requirements.} 223 | 224 | @item{The @racket[#:duration] gives the episode’s duration in seconds, and is @spec{optional but 225 | recommended}.} 226 | 227 | @item{If @racket[_explicit] is an optional override of the mandatory feed-level parental advisory 228 | flag in @racket[podcast]. If it is @racket[_null] (the default), the episode will not contain any 229 | parental advisory information. @spec{If it is @racket[#f], Apple Podcasts will mark the episode as 230 | “Clean”. If it is any other value, Apple Podcasts will mark the episode as “Explicit”.}} 231 | 232 | @item{The @racket[#:episode-num] is optional, but @spec{Apple will require it if the 233 | @racket[podcast] has a type of @racket['episodic].}} 234 | 235 | @item{You can optionally set @racket[#:type] to @racket['full] if this is a normal full-length 236 | episode; to @racket['trailer] for short promos and previews; or to @racket['bonus] for extra or 237 | cross-promotional content.} 238 | 239 | @item{The @racket[#:block] flag can be set to @racket[#t] to prevent a particular episode from 240 | appearing in Apple podcasts. For example you might want to block a specific episode if you know that 241 | its content would otherwise cause the entire podcast to be removed from Apple Podcasts.} 242 | 243 | ] 244 | 245 | } 246 | 247 | @defproc[(podcast [id tag-uri?] 248 | [site-url valid-url-string?] 249 | [name string?] 250 | [episodes (listof episode?)] 251 | [category (or/c string? (list/c string? string?))] 252 | [image-url valid-url-string?] 253 | [owner person?] 254 | [#:explicit? explicit any/c] 255 | [#:type type (or/c 'serial 'episodic #f) #f] 256 | [#:block? block any/c #f] 257 | [#:complete? complete any/c #f] 258 | [#:new-feed-url new-url (or/c valid-url-string? #f) #f]) 259 | podcast?]{ 260 | 261 | Returns a @racketresultfont{#} struct, which can be converted into a feed with 262 | @racket[express-xml]. 263 | 264 | If any of the @tech{tag URIs} of the @racket[_episodes] are @racket[tag=?] with each other or with 265 | the podcast feed @racket[_id], an exception is raised identifying the first duplicate encountered. 266 | 267 | Below are some notes about particular elements supplied to @racket[podcast]. The @spec{colored 268 | passages} indicate things which are required by Apple for inclusion in the Apple Podcasts directory 269 | but which are @emph{not} validated by Splitflap. (See @AppleRequirements[].) 270 | 271 | @itemlist[ 272 | 273 | @item{The @racket[_category] can be either a simple category or a list containing a category and 274 | sub-category. @spec{The category names must be drawn from 275 | @hyperlink["https://podcasters.apple.com/support/1691-apple-podcasts-categories"]{Apple’s podcast 276 | category list}.}} 277 | 278 | @item{The @racket[_image-url] links to the show’s artwork. @spec{It must point to an image with 279 | a minimum size of 1400 ⨉ 1400 pixels and a maximum size of 3000 ⨉ 3000 pixels (the preferred size), 280 | in JPEG or PNG format, 72 dpi, with appropriate file extensions (.jpg, .png), and in the RGB 281 | colorspace.} See @hyperlink["https://podcasters.apple.com/support/896-artwork-requirements"]{Apple 282 | artwork requirements} for other requirements.} 283 | 284 | @item{The @racket[_owner] is for administrative communication about the podcast and is not displayed 285 | in Apple’s podcast listings.} 286 | 287 | @item{The @racket[#:type] can be one of two values. 288 | 289 | @itemlist[ 290 | 291 | @item{Use @racket['episodic] when episodes are not intended to be consumed in any particular order: 292 | @spec{in this case, Apple Podcasts will present newest episodes first. (If organized into seasons, 293 | the newest season will be presented first; otherwise, episodes will be grouped by year published, 294 | newest first.)}} 295 | 296 | @item{Use @racket['serial] when 297 | episodes are intended to be consumed in sequential order: @spec{in this case, Apple Podcasts will 298 | present the oldest episodes first. (If organized into seasons, the newest season will be shown 299 | first.)}} 300 | 301 | ]} 302 | 303 | @item{Setting @racket[#:block] to @racket[#t] will prevent the entire podcast from appearing in 304 | Apple Podcasts.} 305 | 306 | @item{Setting @racket[#:complete?] to @racket[#t] indicates that a podcast is complete and you will 307 | not post any more episodes in the future.} 308 | 309 | @item{If you change the URL where your feed is located, then (in the feed located at the original 310 | URL) set @racket[#:new-feed-url] to the new feed’s URL. Apple Podcasts (and possibly other clients) 311 | will automatically update subscriptions to use the new feed. See 312 | @hyperlink["https://podcasters.apple.com/support/837-change-the-rss-feed-url"]{Apple’s guidelines 313 | for moving your RSS feed}.} 314 | 315 | ]} 316 | 317 | @section{Feed type predicates} 318 | 319 | @defproc[(feed-item? [v any/c]) boolean?]{ 320 | 321 | Returns @racket[#t] if @racket[_v] is a @racket[feed-item] struct, @racket[#f] otherwise. 322 | 323 | } 324 | 325 | @defproc[(feed? [v any/c]) boolean?]{ 326 | 327 | Returns @racket[#t] if @racket[_v] is a @racket[feed] struct, @racket[#f] otherwise. 328 | 329 | } 330 | 331 | @defproc[(episode? [v any/c]) boolean?]{ 332 | 333 | Returns @racket[#t] if @racket[_v] is an @racket[episode] struct, @racket[#f] otherwise. 334 | 335 | } 336 | 337 | @defproc[(podcast? [v any/c]) boolean?]{ 338 | 339 | Returns @racket[#t] if @racket[_v] is a @racket[podcast] struct, @racket[#f] otherwise. 340 | 341 | } 342 | 343 | @defproc[(food? [v any/c]) boolean?]{ 344 | 345 | Returns @racket[#t] when @racket[_v] is one of the struct types that implements the generic 346 | @racket[express-xml] function: @racket[enclosure], @racket[feed-item], @racket[feed], 347 | @racket[episode], or @racket[podcast]; @racket[#f] otherwise} 348 | -------------------------------------------------------------------------------- /splitflap-doc/notes.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/manual 2 | 3 | @(require "misc.rkt" 4 | splitflap/private/version 5 | (for-label splitflap xml)) 6 | 7 | @title{Package Notes (@splitflap-version[])} 8 | 9 | Splitflap can be considered stable. No backward-incompatible changes are planned. 10 | 11 | @section{Known Issues} 12 | 13 | See Splitflap’s @hyperlink["https://github.com/otherjoel/splitflap/issues"]{issues tracker on Github} for any 14 | known problems with the library, or to report problems. 15 | 16 | @subsection{Non-ASCII email addresses and domains} 17 | 18 | The @Atom1.0[] and @RSS2.0[] specs do not contemplate or allow for the use of non-ASCII characters 19 | in email addresses or domain names, which is a defect considering that non-English alphabets are in 20 | widespread use for both of these things. 21 | 22 | At this early stage I have chosen to enforce the standards as written, for consistency’s sake. I do 23 | plan to add a parameter which would cause @racket[dns-domain?] and @racket[email-address?] to 24 | validate strings according to some alternative scheme that allows for non-ASCII characters. In order 25 | to do this correctly, though, I need to educate myself about any standards that exist in this area. 26 | 27 | @section{Version History} 28 | 29 | @subsection{Version 1.2} 30 | 31 | @itemlist[#:style 'compact 32 | 33 | @item{Fix exception raised in @racket[express-xml] when no entries are present 34 | (@link["https://github.com/otherjoel/splitflap/issues/9"]{#9})} 35 | 36 | @item{Added zero-arity form of @racket[infer-moment] to get current moment} 37 | 38 | ] 39 | 40 | @subsection{Version 1.1} 41 | 42 | @itemlist[#:style 'compact 43 | 44 | @item{Fix unquoting bug in @racket[person] x-expressions 45 | (@link["https://github.com/otherjoel/splitflap/pull/7"]{#7})} 46 | 47 | @item{Remove dependency on @racketmodname[txexpr] package 48 | (@link["https://github.com/otherjoel/splitflap/pull/8"]{#8})} 49 | 50 | @item{Ensure @racket[system-language] works with Racket CS 8.4+ 51 | (@link["https://github.com/otherjoel/splitflap/commit/0da67ccdc7c0e7f84c5a34cd88f627d65fbb86f4"]{@tt{0da67ccd}})} 52 | 53 | ] 54 | 55 | 56 | @section{Licensing} 57 | 58 | Splitflap is provided under the terms of the 59 | @hyperlink["https://github.com/otherjoel/splitflap/blob/main/LICENSE.md"]{Blue Oak 1.0.0 license}. 60 | 61 | The split-flap animation in the HTML edition of this documentation comes from the 62 | @hyperlink["https://github.com/rjkerrison/ticker-board"]{ticker-board} project by 63 | Robin James Kerrison, under the terms of the 64 | @hyperlink["https://github.com/otherjoel/splitflap/blob/main/NOTICE.md"]{MIT license.} 65 | -------------------------------------------------------------------------------- /splitflap-doc/splitflap.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/manual 2 | 3 | @(require "misc.rkt") 4 | 5 | @title[#:style 'toc]{Splitflap: Atom and RSS Feeds} 6 | @author[(author+email "Joel Dueck" "joel@jdueck.net")] 7 | 8 | @flipboard-div 9 | @flipboard-script 10 | 11 | @defmodule[splitflap #:use-sources (splitflap/private/feed)] 12 | 13 | This library provides a simple interface for building valid Atom and RSS feeds, including podcast 14 | feeds, for your web properties. 15 | 16 | It’s not mechanically difficult to generate feeds without any special libraries, but there are a lot 17 | of tedious details to get right. Syndication feeds involve several layers of standards and 18 | specifications about how different types data should be encoded, what elements are required, and so 19 | forth. Adhering strictly to those standards is not only good citizenship, it’s the best way of 20 | preventing problems for your subscribers. 21 | 22 | With this library, you are only made to supply the minimum set of data needed to produce a feed. 23 | But everything you supply is carefully validated, so that the result is either a fully valid feed or 24 | an exception. 25 | 26 | Please report any problems on @hyperlink["https://github.com/otherjoel/splitflap/issues"]{the Github 27 | repo}. 28 | 29 | @bold{Installation:} Splitflap requires Racket 8.1 or higher. To install this package from the 30 | command line: 31 | 32 | @terminal{ 33 | @:>{raco pkg install splitflap}} 34 | 35 | Or using DrRacket: click the @onscreen{File} menu → @onscreen{Install Package …}. 36 | 37 | If deploying Splitflap in a production environment, you will probably want to use @tt{splitflap-lib} 38 | instead of @tt{splitflap}. This will avoid fetching/building this documentation, and will greatly 39 | reduce the number of dependencies installed. 40 | 41 | @local-table-of-contents[] 42 | 43 | @include-section["tutorial.scrbl"] 44 | @include-section["mod-splitflap.scrbl"] 45 | @include-section["constructs.scrbl"] 46 | @include-section["notes.scrbl"] 47 | -------------------------------------------------------------------------------- /splitflap-doc/styles/flappy.css: -------------------------------------------------------------------------------- 1 | .board li { 2 | margin: 0; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /splitflap-doc/styles/flappy.tex: -------------------------------------------------------------------------------- 1 | \newcommand{\flipboard}[1]{#1} 2 | -------------------------------------------------------------------------------- /splitflap-doc/styles/terminal.css: -------------------------------------------------------------------------------- 1 | .terminal { 2 | margin-bottom: 1em; 3 | padding: 0.5em; 4 | width: 88%; 5 | background: #fafafa; 6 | } 7 | 8 | .terminal .SIntrapara { 9 | margin: 0 0 0 0; 10 | } 11 | -------------------------------------------------------------------------------- /splitflap-doc/styles/terminal.tex: -------------------------------------------------------------------------------- 1 | \newcommand{\terminal}[1]{#1} 2 | -------------------------------------------------------------------------------- /splitflap-doc/tutorial.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/manual 2 | 3 | @(require 4 | scribble/examples 5 | (for-label splitflap racket/base)) 6 | 7 | @title{Quick start} 8 | 9 | There are four simple steps to building a feed with this library: 10 | 11 | @(define tutorial (make-base-eval #:lang 'racket/base)) 12 | @(tutorial '(require splitflap)) 13 | 14 | @section{Step 1: Mint a tag URI} 15 | 16 | Every feed needs a globally unique identifier, and this library requires you to use @tech{tag URIs} 17 | for this purpose. To mint a tag URI, you provide three things: a domain (or an email address); a 18 | date, and a specific identifier: 19 | 20 | @(examples 21 | #:eval tutorial 22 | #:label #false 23 | (define my-id (mint-tag-uri "example.com" "2012" "blog"))) 24 | 25 | The idiomatic route is to create a tag URI for the entire feed, and then append to that URI to 26 | create tag URIs for the individual items in that feed. 27 | 28 | See @tech{Tag URIs} for more information. 29 | 30 | @section{Step 2: Create a list of items} 31 | 32 | You’ll generally want to write a function that converts your item data from its original format into 33 | @racket[feed-item]s, and then create your list by mapping that function over each of the items. 34 | 35 | For this tutorial, we’ll manually make a list with just one @racket[feed-item] in it: 36 | 37 | @(examples 38 | #:eval tutorial 39 | #:label #false 40 | (define my-items 41 | (list 42 | (feed-item 43 | (append-specific my-id "first-post") (code:comment @#,elem{item-specific ID}) 44 | "https://example.com/first-post.html" (code:comment @#,elem{URL}) 45 | "Chaucer, Rabelais and Balzac" (code:comment @#,elem{title}) 46 | (person "Marian Paroo" "marian@example.com") (code:comment @#,elem{author}) 47 | (infer-moment "1912-06-21") (code:comment @#,elem{publish date}) 48 | (infer-moment "1912-06-21") (code:comment @#,elem{updated date}) 49 | '(article (p "My first post; content TK")))))) 50 | 51 | 52 | @section{Step 3: Create your feed} 53 | 54 | The @racket[feed] struct combines all the elements we’ve created so far: 55 | 56 | @(examples 57 | #:eval tutorial 58 | #:label #false 59 | (define my-feed 60 | (feed 61 | my-id (code:comment @#,elem{tag URI}) 62 | "http://example.com/blog" (code:comment @#,elem{site URL}) 63 | "River City Library Blog" (code:comment @#,elem{Title}) 64 | my-items))) 65 | 66 | @section{Step 4: Generate the XML for your feed} 67 | 68 | Final step: pass your feed to @racket[express-xml], specifying either the @racket['atom] or 69 | @racket['rss] format, and provide a URL where the feed itself will be accessible: 70 | 71 | @(examples 72 | #:eval tutorial 73 | #:label #false 74 | (display (express-xml my-feed 'atom "https://example.com/feed.atom"))) 75 | 76 | There you go! Save that string in a file and you’ve got yourself a valid Atom 1.0 feed. 77 | 78 | Let’s do one in RSS format, for kicks. Note the different URL for this version of the feed: 79 | 80 | @(examples 81 | #:eval tutorial 82 | #:label #false 83 | (display (express-xml my-feed 'rss "https://example.com/feed.rss"))) 84 | 85 | If you want the result as an X-expression, you can do that too, using the @racket[#:as] keyword 86 | argument: 87 | 88 | @(examples 89 | #:eval tutorial 90 | #:label #false 91 | (express-xml my-feed 'rss "https://example.com/feed.rss" #:as 'xexpr)) 92 | 93 | @section{Wrap up} 94 | 95 | Now you know how to use this library to create a feed for your website. 96 | 97 | To create a podcast feed, just use @racket[episode] instead of @racket[feed-item], and 98 | @racket[podcast] instead of @racket[feed]. Check out the module reference for details of using those 99 | functions. 100 | -------------------------------------------------------------------------------- /splitflap-lib/constructs.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | ;; 4 | ;; Predicates and constructors for Dates, Persons, Tag URIs, URLs, email addresses and enclosures 5 | 6 | (require "private/validation.rkt" 7 | (only-in "private/dust.rkt" txexpr?) 8 | gregor 9 | racket/contract) 10 | 11 | (provide dns-domain? 12 | valid-url-string? 13 | email-address? 14 | validate-email-address 15 | 16 | (contract-out 17 | [url-domain (-> valid-url-string? dns-domain?)] 18 | [url-join (-> valid-url-string? relative-path? valid-url-string?)]) 19 | 20 | ; Tag URIs: 21 | tag-entity-date? 22 | tag-uri? 23 | tag-specific-string? 24 | (contract-out 25 | [mint-tag-uri (-> (or/c dns-domain? email-address?) tag-entity-date? tag-specific-string? tag-uri?)] 26 | [append-specific (-> tag-uri? tag-specific-string? tag-uri?)] 27 | [tag-uri->string (->* (tag-uri?) (#:specific tag-specific-string?) string?)] 28 | [tag=? (-> tag-uri? tag-uri? boolean?)] 29 | [normalize-tag-specific (-> string? tag-specific-string?)]) 30 | 31 | ; RSS Dialects 32 | rss-dialect? 33 | 34 | ; Persons 35 | person? 36 | (contract-out 37 | [person (->* (string? email-address?) ((or/c valid-url-string? #f)) person?)] 38 | [person->xexpr (-> person? symbol? (or/c rss-dialect? 'itunes) txexpr?)]) 39 | 40 | ; Moments 41 | (contract-out 42 | [infer-moment (->* () (string?) moment?)] 43 | [moment->string (-> moment? (or/c 'rss 'atom) string?)]) 44 | 45 | ; Enclosures and MIME types 46 | (struct-out enclosure) 47 | file->enclosure 48 | (contract-out 49 | [mime-types-by-ext (promise/c (hash/c symbol? string? #:immutable #t))] 50 | [path/string->mime-type (-> path-string? (or/c string? #f))]) 51 | 52 | ; Language codes 53 | language-codes 54 | iso-639-language-code? 55 | (contract-out 56 | [system-language (promise/c iso-639-language-code?)])) 57 | -------------------------------------------------------------------------------- /splitflap-lib/info.rkt: -------------------------------------------------------------------------------- 1 | #lang info 2 | 3 | (define collection "splitflap") 4 | (define version "1.2") 5 | 6 | (define deps '(["base" #:version "8.1"] 7 | "gregor-lib")) 8 | 9 | (define compile-omit-paths '("private/build.rkt")) 10 | (define pkg-desc "implementation part of \"splitflap\"") 11 | (define license 'BlueOak-1.0.0) 12 | -------------------------------------------------------------------------------- /splitflap-lib/main.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | (require "constructs.rkt" 4 | "private/feed.rkt") 5 | 6 | (provide (all-from-out "constructs.rkt") 7 | (all-from-out "private/feed.rkt")) -------------------------------------------------------------------------------- /splitflap-lib/private/build.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | (require json 4 | net/url 5 | racket/file 6 | racket/list 7 | racket/match 8 | racket/port 9 | racket/runtime-path 10 | racket/string) 11 | 12 | ;; Utility file, not loaded by the rest of the library, used only for infrequent 13 | ;; manual updates of entities.rktd and mime-types.rktd 14 | 15 | (define-runtime-path entities.rktd "./entities.rktd") 16 | 17 | ;; Download the W3C list of HTML entities and serialize as a hash table. 18 | ;; Converts: "Á": { "codepoints": [193], "characters": "\u00C1" }, ... 19 | ;; To: #hash(("aacute" . (193)) ... ) 20 | 21 | ;; Commented out to emphasize that this should only be run manually. 22 | ;; This function is never “armed” in the public package! 23 | #;(define (download-entities) 24 | (define json-url (string->url "https://html.spec.whatwg.org/entities.json")) 25 | (define entities (string->jsexpr (port->string (get-pure-port json-url)))) 26 | (with-output-to-file entities.rktd #:exists 'replace 27 | (lambda () 28 | (write 29 | (for/hash ([(raw-entity v) (in-hash entities)]) 30 | (define entity (string-trim (string-downcase (symbol->string raw-entity)) #rx"&|;")) 31 | (cond 32 | [(member entity '("amp" "lt" "gt" "quot" "apos")) 33 | ;; These five are valid in XML, so use symbols for their codepoint values 34 | (values entity (list (string->symbol entity)))] 35 | [else (values entity (hash-ref v 'codepoints))])))))) 36 | 37 | (define-runtime-path mime-types.rktd "./mime-types.rktd") 38 | 39 | ;; Download the Apache’s public domain list of MIME types and serialize as a hash table. 40 | ;; Converts: application/epub+zip epub 41 | ;; To: #hasheq(("epub" . "application/epub+zip") ... ) 42 | 43 | ;; Commented out to emphasize that this should only be run manually. 44 | ;; This function is never “armed” in the public package! 45 | #;(define (download-mime-types) 46 | (define mime-types-url (string->url "https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types")) 47 | (define mimetypes-lines (port->lines (get-pure-port mime-types-url))) 48 | (define extensions-table 49 | (let loop ([lines mimetypes-lines] 50 | [extensions (hasheq)]) 51 | (cond 52 | [(null? lines) extensions] 53 | [(string-prefix? (car lines) "#") (loop (cdr lines) extensions)] 54 | [else 55 | (match-let* ([(cons line remaining) lines] 56 | [(list mime-type exts ...) (regexp-match* #px"(\\S+)" line)]) 57 | (define new-extensions (append-map (lambda (ext) (list (string->symbol ext) mime-type)) exts)) 58 | (loop remaining (apply hash-set* extensions new-extensions)))]))) 59 | 60 | (with-output-to-file mime-types.rktd #:exists 'replace 61 | (lambda () (write extensions-table)))) 62 | 63 | (module+ test) 64 | -------------------------------------------------------------------------------- /splitflap-lib/private/dust.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | (require (for-syntax racket/base) 4 | (only-in net/url url? url->string) 5 | racket/contract 6 | racket/file 7 | racket/list 8 | racket/match 9 | racket/port 10 | racket/runtime-path 11 | racket/string 12 | xml) 13 | 14 | (provide (all-defined-out)) 15 | 16 | (define-syntax (define-explained-contract stx) 17 | (syntax-case stx () 18 | [(_ (NAME VAL) EXPECTED TEST-EXPR) 19 | #'(define NAME 20 | (flat-contract-with-explanation 21 | (λ (VAL) 22 | (cond 23 | [TEST-EXPR] 24 | [else 25 | (λ (blame) 26 | (raise-blame-error blame VAL '(expected: EXPECTED given: "~e") VAL))])) 27 | #:name 'NAME))])) 28 | 29 | ;; ~~ Tagged X-expressions ~~~~~~~~~~~~~~~~~~~~~~~ 30 | 31 | ;; Lightweight functions for internal use only; avoids a dependency on txexpr package. 32 | 33 | (define-explained-contract (txexpr? v) 34 | "tagged X-expression" 35 | (and (list? v) (xexpr? v))) 36 | 37 | (define (get-attrs tx) 38 | (match tx 39 | [(list* (? symbol?) (list (list (? symbol?) (? string?)) ...) elems) (cadr tx)] 40 | [(list* (? symbol?) elems) '()])) 41 | 42 | (define (get-elements tx) 43 | (match tx 44 | [(list* tag (list (list (? symbol?) (? string?)) ...) elements) elements] 45 | [(list* tag elements) elements])) 46 | 47 | 48 | 49 | ;; ~~ XML Utility functions ~~~~~~~~~~~~~~~~~~~~~~ 50 | 51 | ;; Create CDATA from a string or txexpr and undo any entity escaping in the result. 52 | (define (as-cdata v) 53 | (define entities (hash "&" "&" "<" "<" ">" ">")) 54 | (define cdata-str 55 | (cond [(txexpr? v) 56 | (regexp-replace* #rx"&|<|>" (xexpr->string v) (lambda (e) (hash-ref entities e)))] 57 | [else v])) 58 | (cdata 'racket 'racket (format "" (string-replace cdata-str "]]>" "]]>")))) 59 | 60 | ;; Optional path to XSLT stylesheet 61 | (define feed-xslt-stylesheet (make-parameter #f)) 62 | 63 | ;; Convert an x-expression to a complete XML document 64 | (define (xml-document xpr) 65 | (define xslt-path 66 | (match (feed-xslt-stylesheet) 67 | [(? url? u) (url->string u)] 68 | [v v])) 69 | (define xml-processing 70 | (cons (p-i 'racket 'racket 'xml "version=\"1.0\" encoding=\"UTF-8\"") 71 | (if xslt-path 72 | (list (p-i 'racket 'racket 'xml-stylesheet 73 | (format "href=\"~a\" type=\"text/xsl\"" xslt-path))) 74 | '()))) 75 | (document 76 | (prolog xml-processing #f '()) 77 | (xexpr->xml xpr) 78 | '())) 79 | 80 | ;; ~~ HTML entity escaping ~~~~~~~~~~~~~~~~~~~~~~~ 81 | 82 | ;; Named character entities other than the five in the XML spec (< > & ' ") 83 | ;; must be replaced with their numeric equivalents 84 | 85 | (define-runtime-path entities.rktd "./entities.rktd") 86 | (define entities (file->value entities.rktd)) ;; (Generated by build.rkt) 87 | 88 | ;; Replace any named character references with numeric ones. 89 | ;; (-> string? (listof (or/c string? symbol? valid-char?))) 90 | (define (numberize-named-entities str) 91 | (match (regexp-match-positions* #rx"&[a-zA-Z]+;" str) 92 | ['() 93 | (list str)] 94 | [to-escape 95 | (let loop ([gap-end (string-length str)] 96 | [to-escape (reverse to-escape)] 97 | [acc null]) 98 | (match to-escape 99 | ['() 100 | (if (zero? gap-end) 101 | acc 102 | (cons (substring str 0 gap-end) 103 | acc))] 104 | [(cons (cons pos gap-start) to-escape) 105 | (define new-acc 106 | (if (= gap-start gap-end) 107 | acc 108 | (cons (substring str gap-start gap-end) 109 | acc))) 110 | (define normalized-entity 111 | (string-trim (string-downcase (substring str pos gap-start)) #rx"&|;")) 112 | (loop pos 113 | to-escape 114 | (match (hash-ref entities normalized-entity #f) 115 | ;; Most entities equate to a single codepoint, but some equate to two 116 | [(list cpoint) 117 | (cons cpoint new-acc)] 118 | [(list cpoint1 cpoint2) 119 | (cons cpoint1 (cons cpoint2 new-acc))] 120 | [_ (cons (substring str pos gap-start) new-acc)]))]))])) 121 | 122 | ;; Converts a tagged X-expression into an XML string, with any non-XML entities in string elements 123 | ;; replaced with numeric equivalents. This string can in turn be used as the content of another 124 | ;; X-expression element with the type="html" attribute, so that when this second X-expression is 125 | ;; rendered as a string, the result is escaped HTML containing only valid XML entities. 126 | (define (txexpr->safe-content-str tx) 127 | (xexpr->string 128 | (let loop ([x tx]) 129 | (cond 130 | [(txexpr? x) 131 | `(,(car x) 132 | ,@(list (get-attrs x)) 133 | ,@(append-map (λ (c) 134 | (if (string? c) 135 | (numberize-named-entities c) 136 | (list (loop c)))) 137 | (get-elements x)))] 138 | [else x])))) 139 | 140 | (define (content->safe-element content element dialect preserve-cdata-struct?) 141 | `(,element 142 | ,@(if/sp (and (txexpr? content) (eq? dialect 'atom)) `[[type "html"]]) 143 | ,(cond 144 | [(string? content) 145 | (let ([result (as-cdata content)]) (if preserve-cdata-struct? result (cdata-string result)))] 146 | [else (txexpr->safe-content-str content)]))) 147 | 148 | 149 | ;; ~~ XML Display ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 150 | 151 | ;; Convert an xexpr to a string with escaped and nicely-indented XML. 152 | (define (indented-xml-string xpr #:document? [doc? #f]) 153 | (define display-proc (if doc? display-xml display-xml/content)) 154 | (define xml-str 155 | (with-output-to-string 156 | (λ () 157 | (parameterize ([empty-tag-shorthand 'always]) 158 | (display-proc (if doc? (xml-document xpr) (xexpr->xml xpr)) #:indentation 'peek))))) 159 | (string-trim xml-str #:right? #f)) 160 | 161 | 162 | 163 | ;; ~~ Other stuff ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 164 | 165 | ;; Handy for splicing something or nothing into xexprs 166 | (define-syntax (if/sp stx) 167 | (syntax-case stx () 168 | [(_ check val) #'(if check (list val) '())])) -------------------------------------------------------------------------------- /splitflap-lib/private/entities.rktd: -------------------------------------------------------------------------------- 1 | #hash(("aacute" . (193)) ("abreve" . (259)) ("ac" . (8766)) ("acd" . (8767)) ("ace" . (8766 819)) ("acirc" . (194)) ("acute" . (180)) ("acy" . (1072)) ("aelig" . (230)) ("af" . (8289)) ("afr" . (120068)) ("agrave" . (192)) ("alefsym" . (8501)) ("aleph" . (8501)) ("alpha" . (945)) ("amacr" . (257)) ("amalg" . (10815)) ("amp" . (amp)) ("and" . (10835)) ("andand" . (10837)) ("andd" . (10844)) ("andslope" . (10840)) ("andv" . (10842)) ("ang" . (8736)) ("ange" . (10660)) ("angle" . (8736)) ("angmsd" . (8737)) ("angmsdaa" . (10664)) ("angmsdab" . (10665)) ("angmsdac" . (10666)) ("angmsdad" . (10667)) ("angmsdae" . (10668)) ("angmsdaf" . (10669)) ("angmsdag" . (10670)) ("angmsdah" . (10671)) ("angrt" . (8735)) ("angrtvb" . (8894)) ("angrtvbd" . (10653)) ("angsph" . (8738)) ("angst" . (197)) ("angzarr" . (9084)) ("aogon" . (261)) ("aopf" . (120146)) ("ap" . (8776)) ("apacir" . (10863)) ("ape" . (8778)) ("apid" . (8779)) ("apos" . (apos)) ("applyfunction" . (8289)) ("approx" . (8776)) ("approxeq" . (8778)) ("aring" . (197)) ("ascr" . (119990)) ("assign" . (8788)) ("ast" . (42)) ("asymp" . (8776)) ("asympeq" . (8781)) ("atilde" . (227)) ("auml" . (196)) ("awconint" . (8755)) ("awint" . (10769)) ("backcong" . (8780)) ("backepsilon" . (1014)) ("backprime" . (8245)) ("backsim" . (8765)) ("backsimeq" . (8909)) ("backslash" . (8726)) ("barv" . (10983)) ("barvee" . (8893)) ("barwed" . (8966)) ("barwedge" . (8965)) ("bbrk" . (9141)) ("bbrktbrk" . (9142)) ("bcong" . (8780)) ("bcy" . (1041)) ("bdquo" . (8222)) ("becaus" . (8757)) ("because" . (8757)) ("bemptyv" . (10672)) ("bepsi" . (1014)) ("bernou" . (8492)) ("bernoullis" . (8492)) ("beta" . (914)) ("beth" . (8502)) ("between" . (8812)) ("bfr" . (120095)) ("bigcap" . (8898)) ("bigcirc" . (9711)) ("bigcup" . (8899)) ("bigodot" . (10752)) ("bigoplus" . (10753)) ("bigotimes" . (10754)) ("bigsqcup" . (10758)) ("bigstar" . (9733)) ("bigtriangledown" . (9661)) ("bigtriangleup" . (9651)) ("biguplus" . (10756)) ("bigvee" . (8897)) ("bigwedge" . (8896)) ("bkarow" . (10509)) ("blacklozenge" . (10731)) ("blacksquare" . (9642)) ("blacktriangle" . (9652)) ("blacktriangledown" . (9662)) ("blacktriangleleft" . (9666)) ("blacktriangleright" . (9656)) ("blank" . (9251)) ("blk12" . (9618)) ("blk14" . (9617)) ("blk34" . (9619)) ("block" . (9608)) ("bne" . (61 8421)) ("bnequiv" . (8801 8421)) ("bnot" . (10989)) ("bopf" . (120121)) ("bot" . (8869)) ("bottom" . (8869)) ("bowtie" . (8904)) ("boxbox" . (10697)) ("boxdl" . (9558)) ("boxdr" . (9556)) ("boxh" . (9552)) ("boxhd" . (9572)) ("boxhu" . (9577)) ("boxminus" . (8863)) ("boxplus" . (8862)) ("boxtimes" . (8864)) ("boxul" . (9565)) ("boxur" . (9561)) ("boxv" . (9474)) ("boxvh" . (9578)) ("boxvl" . (9570)) ("boxvr" . (9568)) ("bprime" . (8245)) ("breve" . (728)) ("brvbar" . (166)) ("bscr" . (119991)) ("bsemi" . (8271)) ("bsim" . (8765)) ("bsime" . (8909)) ("bsol" . (92)) ("bsolb" . (10693)) ("bsolhsub" . (10184)) ("bull" . (8226)) ("bullet" . (8226)) ("bump" . (8782)) ("bumpe" . (10926)) ("bumpeq" . (8782)) ("cacute" . (262)) ("cap" . (8914)) ("capand" . (10820)) ("capbrcup" . (10825)) ("capcap" . (10827)) ("capcup" . (10823)) ("capdot" . (10816)) ("capitaldifferentiald" . (8517)) ("caps" . (8745 65024)) ("caret" . (8257)) ("caron" . (711)) ("cayleys" . (8493)) ("ccaps" . (10829)) ("ccaron" . (269)) ("ccedil" . (199)) ("ccirc" . (265)) ("cconint" . (8752)) ("ccups" . (10828)) ("ccupssm" . (10832)) ("cdot" . (266)) ("cedil" . (184)) ("cedilla" . (184)) ("cemptyv" . (10674)) ("cent" . (162)) ("centerdot" . (183)) ("cfr" . (120096)) ("chcy" . (1095)) ("check" . (10003)) ("checkmark" . (10003)) ("chi" . (967)) ("cir" . (9675)) ("circ" . (710)) ("circeq" . (8791)) ("circlearrowleft" . (8634)) ("circlearrowright" . (8635)) ("circledast" . (8859)) ("circledcirc" . (8858)) ("circleddash" . (8861)) ("circledot" . (8857)) ("circledr" . (174)) ("circleds" . (9416)) ("circleminus" . (8854)) ("circleplus" . (8853)) ("circletimes" . (8855)) ("cire" . (10691)) ("cirfnint" . (10768)) ("cirmid" . (10991)) ("cirscir" . (10690)) ("clockwisecontourintegral" . (8754)) ("closecurlydoublequote" . (8221)) ("closecurlyquote" . (8217)) ("clubs" . (9827)) ("clubsuit" . (9827)) ("colon" . (8759)) ("colone" . (8788)) ("coloneq" . (8788)) ("comma" . (44)) ("commat" . (64)) ("comp" . (8705)) ("compfn" . (8728)) ("complement" . (8705)) ("complexes" . (8450)) ("cong" . (8773)) ("congdot" . (10861)) ("congruent" . (8801)) ("conint" . (8751)) ("contourintegral" . (8750)) ("copf" . (120148)) ("coprod" . (8720)) ("coproduct" . (8720)) ("copy" . (169)) ("copysr" . (8471)) ("counterclockwisecontourintegral" . (8755)) ("crarr" . (8629)) ("cross" . (10799)) ("cscr" . (119992)) ("csub" . (10959)) ("csube" . (10961)) ("csup" . (10960)) ("csupe" . (10962)) ("ctdot" . (8943)) ("cudarrl" . (10552)) ("cudarrr" . (10549)) ("cuepr" . (8926)) ("cuesc" . (8927)) ("cularr" . (8630)) ("cularrp" . (10557)) ("cup" . (8915)) ("cupbrcap" . (10824)) ("cupcap" . (8781)) ("cupcup" . (10826)) ("cupdot" . (8845)) ("cupor" . (10821)) ("cups" . (8746 65024)) ("curarr" . (8631)) ("curarrm" . (10556)) ("curlyeqprec" . (8926)) ("curlyeqsucc" . (8927)) ("curlyvee" . (8910)) ("curlywedge" . (8911)) ("curren" . (164)) ("curvearrowleft" . (8630)) ("curvearrowright" . (8631)) ("cuvee" . (8910)) ("cuwed" . (8911)) ("cwconint" . (8754)) ("cwint" . (8753)) ("cylcty" . (9005)) ("dagger" . (8224)) ("daleth" . (8504)) ("darr" . (8609)) ("dash" . (8208)) ("dashv" . (10980)) ("dbkarow" . (10511)) ("dblac" . (733)) ("dcaron" . (271)) ("dcy" . (1044)) ("dd" . (8518)) ("ddagger" . (8225)) ("ddarr" . (8650)) ("ddotrahd" . (10513)) ("ddotseq" . (10871)) ("deg" . (176)) ("del" . (8711)) ("delta" . (948)) ("demptyv" . (10673)) ("dfisht" . (10623)) ("dfr" . (120097)) ("dhar" . (10597)) ("dharl" . (8643)) ("dharr" . (8642)) ("diacriticalacute" . (180)) ("diacriticaldot" . (729)) ("diacriticaldoubleacute" . (733)) ("diacriticalgrave" . (96)) ("diacriticaltilde" . (732)) ("diam" . (8900)) ("diamond" . (8900)) ("diamondsuit" . (9830)) ("diams" . (9830)) ("die" . (168)) ("differentiald" . (8518)) ("digamma" . (989)) ("disin" . (8946)) ("div" . (247)) ("divide" . (247)) ("divideontimes" . (8903)) ("divonx" . (8903)) ("djcy" . (1026)) ("dlcorn" . (8990)) ("dlcrop" . (8973)) ("dollar" . (36)) ("dopf" . (120123)) ("dot" . (168)) ("dotdot" . (8412)) ("doteq" . (8784)) ("doteqdot" . (8785)) ("dotequal" . (8784)) ("dotminus" . (8760)) ("dotplus" . (8724)) ("dotsquare" . (8865)) ("doublebarwedge" . (8966)) ("doublecontourintegral" . (8751)) ("doubledot" . (168)) ("doubledownarrow" . (8659)) ("doubleleftarrow" . (8656)) ("doubleleftrightarrow" . (8660)) ("doublelefttee" . (10980)) ("doublelongleftarrow" . (10232)) ("doublelongleftrightarrow" . (10234)) ("doublelongrightarrow" . (10233)) ("doublerightarrow" . (8658)) ("doublerighttee" . (8872)) ("doubleuparrow" . (8657)) ("doubleupdownarrow" . (8661)) ("doubleverticalbar" . (8741)) ("downarrow" . (8595)) ("downarrowbar" . (10515)) ("downarrowuparrow" . (8693)) ("downbreve" . (785)) ("downdownarrows" . (8650)) ("downharpoonleft" . (8643)) ("downharpoonright" . (8642)) ("downleftrightvector" . (10576)) ("downleftteevector" . (10590)) ("downleftvector" . (8637)) ("downleftvectorbar" . (10582)) ("downrightteevector" . (10591)) ("downrightvector" . (8641)) ("downrightvectorbar" . (10583)) ("downtee" . (8868)) ("downteearrow" . (8615)) ("drbkarow" . (10512)) ("drcorn" . (8991)) ("drcrop" . (8972)) ("dscr" . (119967)) ("dscy" . (1029)) ("dsol" . (10742)) ("dstrok" . (272)) ("dtdot" . (8945)) ("dtri" . (9663)) ("dtrif" . (9662)) ("duarr" . (8693)) ("duhar" . (10607)) ("dwangle" . (10662)) ("dzcy" . (1119)) ("dzigrarr" . (10239)) ("eacute" . (233)) ("easter" . (10862)) ("ecaron" . (283)) ("ecir" . (8790)) ("ecirc" . (234)) ("ecolon" . (8789)) ("ecy" . (1069)) ("eddot" . (10871)) ("edot" . (8785)) ("ee" . (8519)) ("efdot" . (8786)) ("efr" . (120098)) ("eg" . (10906)) ("egrave" . (200)) ("egs" . (10902)) ("egsdot" . (10904)) ("el" . (10905)) ("element" . (8712)) ("elinters" . (9191)) ("ell" . (8467)) ("els" . (10901)) ("elsdot" . (10903)) ("emacr" . (275)) ("empty" . (8709)) ("emptyset" . (8709)) ("emptysmallsquare" . (9723)) ("emptyv" . (8709)) ("emptyverysmallsquare" . (9643)) ("emsp" . (8195)) ("emsp13" . (8196)) ("emsp14" . (8197)) ("eng" . (331)) ("ensp" . (8194)) ("eogon" . (281)) ("eopf" . (120124)) ("epar" . (8917)) ("eparsl" . (10723)) ("eplus" . (10865)) ("epsi" . (949)) ("epsilon" . (917)) ("epsiv" . (1013)) ("eqcirc" . (8790)) ("eqcolon" . (8789)) ("eqsim" . (8770)) ("eqslantgtr" . (10902)) ("eqslantless" . (10901)) ("equal" . (10869)) ("equals" . (61)) ("equaltilde" . (8770)) ("equest" . (8799)) ("equilibrium" . (8652)) ("equiv" . (8801)) ("equivdd" . (10872)) ("eqvparsl" . (10725)) ("erarr" . (10609)) ("erdot" . (8787)) ("escr" . (8496)) ("esdot" . (8784)) ("esim" . (8770)) ("eta" . (919)) ("eth" . (240)) ("euml" . (203)) ("euro" . (8364)) ("excl" . (33)) ("exist" . (8707)) ("exists" . (8707)) ("expectation" . (8496)) ("exponentiale" . (8519)) ("fallingdotseq" . (8786)) ("fcy" . (1060)) ("female" . (9792)) ("ffilig" . (64259)) ("fflig" . (64256)) ("ffllig" . (64260)) ("ffr" . (120099)) ("filig" . (64257)) ("filledsmallsquare" . (9724)) ("filledverysmallsquare" . (9642)) ("fjlig" . (102 106)) ("flat" . (9837)) ("fllig" . (64258)) ("fltns" . (9649)) ("fnof" . (402)) ("fopf" . (120151)) ("forall" . (8704)) ("fork" . (8916)) ("forkv" . (10969)) ("fouriertrf" . (8497)) ("fpartint" . (10765)) ("frac12" . (189)) ("frac13" . (8531)) ("frac14" . (188)) ("frac15" . (8533)) ("frac16" . (8537)) ("frac18" . (8539)) ("frac23" . (8532)) ("frac25" . (8534)) ("frac34" . (190)) ("frac35" . (8535)) ("frac38" . (8540)) ("frac45" . (8536)) ("frac56" . (8538)) ("frac58" . (8541)) ("frac78" . (8542)) ("frasl" . (8260)) ("frown" . (8994)) ("fscr" . (8497)) ("gacute" . (501)) ("gamma" . (915)) ("gammad" . (988)) ("gap" . (10886)) ("gbreve" . (287)) ("gcedil" . (290)) ("gcirc" . (284)) ("gcy" . (1075)) ("gdot" . (288)) ("ge" . (8805)) ("gel" . (8923)) ("geq" . (8805)) ("geqq" . (8807)) ("geqslant" . (10878)) ("ges" . (10878)) ("gescc" . (10921)) ("gesdot" . (10880)) ("gesdoto" . (10882)) ("gesdotol" . (10884)) ("gesl" . (8923 65024)) ("gesles" . (10900)) ("gfr" . (120074)) ("gg" . (8811)) ("ggg" . (8921)) ("gimel" . (8503)) ("gjcy" . (1107)) ("gl" . (8823)) ("gla" . (10917)) ("gle" . (10898)) ("glj" . (10916)) ("gnap" . (10890)) ("gnapprox" . (10890)) ("gne" . (10888)) ("gneq" . (10888)) ("gneqq" . (8809)) ("gnsim" . (8935)) ("gopf" . (120126)) ("grave" . (96)) ("greaterequal" . (8805)) ("greaterequalless" . (8923)) ("greaterfullequal" . (8807)) ("greatergreater" . (10914)) ("greaterless" . (8823)) ("greaterslantequal" . (10878)) ("greatertilde" . (8819)) ("gscr" . (8458)) ("gsim" . (8819)) ("gsime" . (10894)) ("gsiml" . (10896)) ("gt" . (gt)) ("gtcc" . (10919)) ("gtcir" . (10874)) ("gtdot" . (8919)) ("gtlpar" . (10645)) ("gtquest" . (10876)) ("gtrapprox" . (10886)) ("gtrarr" . (10616)) ("gtrdot" . (8919)) ("gtreqless" . (8923)) ("gtreqqless" . (10892)) ("gtrless" . (8823)) ("gtrsim" . (8819)) ("gvertneqq" . (8809 65024)) ("gvne" . (8809 65024)) ("hacek" . (711)) ("hairsp" . (8202)) ("half" . (189)) ("hamilt" . (8459)) ("hardcy" . (1066)) ("harr" . (8596)) ("harrcir" . (10568)) ("harrw" . (8621)) ("hat" . (94)) ("hbar" . (8463)) ("hcirc" . (292)) ("hearts" . (9829)) ("heartsuit" . (9829)) ("hellip" . (8230)) ("hercon" . (8889)) ("hfr" . (120101)) ("hilbertspace" . (8459)) ("hksearow" . (10533)) ("hkswarow" . (10534)) ("hoarr" . (8703)) ("homtht" . (8763)) ("hookleftarrow" . (8617)) ("hookrightarrow" . (8618)) ("hopf" . (120153)) ("horbar" . (8213)) ("horizontalline" . (9472)) ("hscr" . (8459)) ("hslash" . (8463)) ("hstrok" . (294)) ("humpdownhump" . (8782)) ("humpequal" . (8783)) ("hybull" . (8259)) ("hyphen" . (8208)) ("iacute" . (205)) ("ic" . (8291)) ("icirc" . (238)) ("icy" . (1048)) ("idot" . (304)) ("iecy" . (1077)) ("iexcl" . (161)) ("iff" . (8660)) ("ifr" . (120102)) ("igrave" . (236)) ("ii" . (8520)) ("iiiint" . (10764)) ("iiint" . (8749)) ("iinfin" . (10716)) ("iiota" . (8489)) ("ijlig" . (307)) ("im" . (8465)) ("imacr" . (299)) ("image" . (8465)) ("imaginaryi" . (8520)) ("imagline" . (8464)) ("imagpart" . (8465)) ("imath" . (305)) ("imof" . (8887)) ("imped" . (437)) ("implies" . (8658)) ("in" . (8712)) ("incare" . (8453)) ("infin" . (8734)) ("infintie" . (10717)) ("inodot" . (305)) ("int" . (8747)) ("intcal" . (8890)) ("integers" . (8484)) ("integral" . (8747)) ("intercal" . (8890)) ("intersection" . (8898)) ("intlarhk" . (10775)) ("intprod" . (10812)) ("invisiblecomma" . (8291)) ("invisibletimes" . (8290)) ("iocy" . (1025)) ("iogon" . (302)) ("iopf" . (120128)) ("iota" . (953)) ("iprod" . (10812)) ("iquest" . (191)) ("iscr" . (8464)) ("isin" . (8712)) ("isindot" . (8949)) ("isine" . (8953)) ("isins" . (8948)) ("isinsv" . (8947)) ("isinv" . (8712)) ("it" . (8290)) ("itilde" . (296)) ("iukcy" . (1030)) ("iuml" . (207)) ("jcirc" . (309)) ("jcy" . (1049)) ("jfr" . (120103)) ("jmath" . (567)) ("jopf" . (120155)) ("jscr" . (119973)) ("jsercy" . (1032)) ("jukcy" . (1028)) ("kappa" . (922)) ("kappav" . (1008)) ("kcedil" . (311)) ("kcy" . (1082)) ("kfr" . (120104)) ("kgreen" . (312)) ("khcy" . (1093)) ("kjcy" . (1036)) ("kopf" . (120156)) ("kscr" . (120000)) ("laarr" . (8666)) ("lacute" . (314)) ("laemptyv" . (10676)) ("lagran" . (8466)) ("lambda" . (923)) ("lang" . (10218)) ("langd" . (10641)) ("langle" . (10216)) ("lap" . (10885)) ("laplacetrf" . (8466)) ("laquo" . (171)) ("larr" . (8606)) ("larrb" . (8676)) ("larrbfs" . (10527)) ("larrfs" . (10525)) ("larrhk" . (8617)) ("larrlp" . (8619)) ("larrpl" . (10553)) ("larrsim" . (10611)) ("larrtl" . (8610)) ("lat" . (10923)) ("latail" . (10521)) ("late" . (10925)) ("lates" . (10925 65024)) ("lbarr" . (10510)) ("lbbrk" . (10098)) ("lbrace" . (123)) ("lbrack" . (91)) ("lbrke" . (10635)) ("lbrksld" . (10639)) ("lbrkslu" . (10637)) ("lcaron" . (317)) ("lcedil" . (315)) ("lceil" . (8968)) ("lcub" . (123)) ("lcy" . (1051)) ("ldca" . (10550)) ("ldquo" . (8220)) ("ldquor" . (8222)) ("ldrdhar" . (10599)) ("ldrushar" . (10571)) ("ldsh" . (8626)) ("le" . (8806)) ("leftanglebracket" . (10216)) ("leftarrow" . (8592)) ("leftarrowbar" . (8676)) ("leftarrowrightarrow" . (8646)) ("leftarrowtail" . (8610)) ("leftceiling" . (8968)) ("leftdoublebracket" . (10214)) ("leftdownteevector" . (10593)) ("leftdownvector" . (8643)) ("leftdownvectorbar" . (10585)) ("leftfloor" . (8970)) ("leftharpoondown" . (8637)) ("leftharpoonup" . (8636)) ("leftleftarrows" . (8647)) ("leftrightarrow" . (8596)) ("leftrightarrows" . (8646)) ("leftrightharpoons" . (8651)) ("leftrightsquigarrow" . (8621)) ("leftrightvector" . (10574)) ("lefttee" . (8867)) ("leftteearrow" . (8612)) ("leftteevector" . (10586)) ("leftthreetimes" . (8907)) ("lefttriangle" . (8882)) ("lefttrianglebar" . (10703)) ("lefttriangleequal" . (8884)) ("leftupdownvector" . (10577)) ("leftupteevector" . (10592)) ("leftupvector" . (8639)) ("leftupvectorbar" . (10584)) ("leftvector" . (8636)) ("leftvectorbar" . (10578)) ("leg" . (8922)) ("leq" . (8804)) ("leqq" . (8806)) ("leqslant" . (10877)) ("les" . (10877)) ("lescc" . (10920)) ("lesdot" . (10879)) ("lesdoto" . (10881)) ("lesdotor" . (10883)) ("lesg" . (8922 65024)) ("lesges" . (10899)) ("lessapprox" . (10885)) ("lessdot" . (8918)) ("lesseqgtr" . (8922)) ("lesseqqgtr" . (10891)) ("lessequalgreater" . (8922)) ("lessfullequal" . (8806)) ("lessgreater" . (8822)) ("lessgtr" . (8822)) ("lessless" . (10913)) ("lesssim" . (8818)) ("lessslantequal" . (10877)) ("lesstilde" . (8818)) ("lfisht" . (10620)) ("lfloor" . (8970)) ("lfr" . (120079)) ("lg" . (8822)) ("lge" . (10897)) ("lhar" . (10594)) ("lhard" . (8637)) ("lharu" . (8636)) ("lharul" . (10602)) ("lhblk" . (9604)) ("ljcy" . (1113)) ("ll" . (8920)) ("llarr" . (8647)) ("llcorner" . (8990)) ("lleftarrow" . (8666)) ("llhard" . (10603)) ("lltri" . (9722)) ("lmidot" . (320)) ("lmoust" . (9136)) ("lmoustache" . (9136)) ("lnap" . (10889)) ("lnapprox" . (10889)) ("lne" . (8808)) ("lneq" . (10887)) ("lneqq" . (8808)) ("lnsim" . (8934)) ("loang" . (10220)) ("loarr" . (8701)) ("lobrk" . (10214)) ("longleftarrow" . (10229)) ("longleftrightarrow" . (10234)) ("longmapsto" . (10236)) ("longrightarrow" . (10233)) ("looparrowleft" . (8619)) ("looparrowright" . (8620)) ("lopar" . (10629)) ("lopf" . (120131)) ("loplus" . (10797)) ("lotimes" . (10804)) ("lowast" . (8727)) ("lowbar" . (95)) ("lowerleftarrow" . (8601)) ("lowerrightarrow" . (8600)) ("loz" . (9674)) ("lozenge" . (9674)) ("lozf" . (10731)) ("lpar" . (40)) ("lparlt" . (10643)) ("lrarr" . (8646)) ("lrcorner" . (8991)) ("lrhar" . (8651)) ("lrhard" . (10605)) ("lrm" . (8206)) ("lrtri" . (8895)) ("lsaquo" . (8249)) ("lscr" . (120001)) ("lsh" . (8624)) ("lsim" . (8818)) ("lsime" . (10893)) ("lsimg" . (10895)) ("lsqb" . (91)) ("lsquo" . (8216)) ("lsquor" . (8218)) ("lstrok" . (322)) ("lt" . (lt)) ("ltcc" . (10918)) ("ltcir" . (10873)) ("ltdot" . (8918)) ("lthree" . (8907)) ("ltimes" . (8905)) ("ltlarr" . (10614)) ("ltquest" . (10875)) ("ltri" . (9667)) ("ltrie" . (8884)) ("ltrif" . (9666)) ("ltrpar" . (10646)) ("lurdshar" . (10570)) ("luruhar" . (10598)) ("lvertneqq" . (8808 65024)) ("lvne" . (8808 65024)) ("macr" . (175)) ("male" . (9794)) ("malt" . (10016)) ("maltese" . (10016)) ("map" . (10501)) ("mapsto" . (8614)) ("mapstodown" . (8615)) ("mapstoleft" . (8612)) ("mapstoup" . (8613)) ("marker" . (9646)) ("mcomma" . (10793)) ("mcy" . (1084)) ("mdash" . (8212)) ("mddot" . (8762)) ("measuredangle" . (8737)) ("mediumspace" . (8287)) ("mellintrf" . (8499)) ("mfr" . (120106)) ("mho" . (8487)) ("micro" . (181)) ("mid" . (8739)) ("midast" . (42)) ("midcir" . (10992)) ("middot" . (183)) ("minus" . (8722)) ("minusb" . (8863)) ("minusd" . (8760)) ("minusdu" . (10794)) ("minusplus" . (8723)) ("mlcp" . (10971)) ("mldr" . (8230)) ("mnplus" . (8723)) ("models" . (8871)) ("mopf" . (120132)) ("mp" . (8723)) ("mscr" . (8499)) ("mstpos" . (8766)) ("mu" . (956)) ("multimap" . (8888)) ("mumap" . (8888)) ("nabla" . (8711)) ("nacute" . (324)) ("nang" . (8736 8402)) ("nap" . (8777)) ("nape" . (10864 824)) ("napid" . (8779 824)) ("napos" . (329)) ("napprox" . (8777)) ("natur" . (9838)) ("natural" . (9838)) ("naturals" . (8469)) ("nbsp" . (160)) ("nbump" . (8782 824)) ("nbumpe" . (8783 824)) ("ncap" . (10819)) ("ncaron" . (327)) ("ncedil" . (325)) ("ncong" . (8775)) ("ncongdot" . (10861 824)) ("ncup" . (10818)) ("ncy" . (1053)) ("ndash" . (8211)) ("ne" . (8800)) ("nearhk" . (10532)) ("nearr" . (8663)) ("nearrow" . (8599)) ("nedot" . (8784 824)) ("negativemediumspace" . (8203)) ("negativethickspace" . (8203)) ("negativethinspace" . (8203)) ("negativeverythinspace" . (8203)) ("nequiv" . (8802)) ("nesear" . (10536)) ("nesim" . (8770 824)) ("nestedgreatergreater" . (8811)) ("nestedlessless" . (8810)) ("newline" . (10)) ("nexist" . (8708)) ("nexists" . (8708)) ("nfr" . (120081)) ("nge" . (8807 824)) ("ngeq" . (8817)) ("ngeqq" . (8807 824)) ("ngeqslant" . (10878 824)) ("nges" . (10878 824)) ("ngg" . (8921 824)) ("ngsim" . (8821)) ("ngt" . (8815)) ("ngtr" . (8815)) ("ngtv" . (8811 824)) ("nharr" . (8654)) ("nhpar" . (10994)) ("ni" . (8715)) ("nis" . (8956)) ("nisd" . (8954)) ("niv" . (8715)) ("njcy" . (1114)) ("nlarr" . (8653)) ("nldr" . (8229)) ("nle" . (8816)) ("nleftarrow" . (8653)) ("nleftrightarrow" . (8622)) ("nleq" . (8816)) ("nleqq" . (8806 824)) ("nleqslant" . (10877 824)) ("nles" . (10877 824)) ("nless" . (8814)) ("nll" . (8920 824)) ("nlsim" . (8820)) ("nlt" . (8810 8402)) ("nltri" . (8938)) ("nltrie" . (8940)) ("nltv" . (8810 824)) ("nmid" . (8740)) ("nobreak" . (8288)) ("nonbreakingspace" . (160)) ("nopf" . (120159)) ("not" . (10988)) ("notcongruent" . (8802)) ("notcupcap" . (8813)) ("notdoubleverticalbar" . (8742)) ("notelement" . (8713)) ("notequal" . (8800)) ("notequaltilde" . (8770 824)) ("notexists" . (8708)) ("notgreater" . (8815)) ("notgreaterequal" . (8817)) ("notgreaterfullequal" . (8807 824)) ("notgreatergreater" . (8811 824)) ("notgreaterless" . (8825)) ("notgreaterslantequal" . (10878 824)) ("notgreatertilde" . (8821)) ("nothumpdownhump" . (8782 824)) ("nothumpequal" . (8783 824)) ("notin" . (8713)) ("notindot" . (8949 824)) ("notine" . (8953 824)) ("notinva" . (8713)) ("notinvb" . (8951)) ("notinvc" . (8950)) ("notlefttriangle" . (8938)) ("notlefttrianglebar" . (10703 824)) ("notlefttriangleequal" . (8940)) ("notless" . (8814)) ("notlessequal" . (8816)) ("notlessgreater" . (8824)) ("notlessless" . (8810 824)) ("notlessslantequal" . (10877 824)) ("notlesstilde" . (8820)) ("notnestedgreatergreater" . (10914 824)) ("notnestedlessless" . (10913 824)) ("notni" . (8716)) ("notniva" . (8716)) ("notnivb" . (8958)) ("notnivc" . (8957)) ("notprecedes" . (8832)) ("notprecedesequal" . (10927 824)) ("notprecedesslantequal" . (8928)) ("notreverseelement" . (8716)) ("notrighttriangle" . (8939)) ("notrighttrianglebar" . (10704 824)) ("notrighttriangleequal" . (8941)) ("notsquaresubset" . (8847 824)) ("notsquaresubsetequal" . (8930)) ("notsquaresuperset" . (8848 824)) ("notsquaresupersetequal" . (8931)) ("notsubset" . (8834 8402)) ("notsubsetequal" . (8840)) ("notsucceeds" . (8833)) ("notsucceedsequal" . (10928 824)) ("notsucceedsslantequal" . (8929)) ("notsucceedstilde" . (8831 824)) ("notsuperset" . (8835 8402)) ("notsupersetequal" . (8841)) ("nottilde" . (8769)) ("nottildeequal" . (8772)) ("nottildefullequal" . (8775)) ("nottildetilde" . (8777)) ("notverticalbar" . (8740)) ("npar" . (8742)) ("nparallel" . (8742)) ("nparsl" . (11005 8421)) ("npart" . (8706 824)) ("npolint" . (10772)) ("npr" . (8832)) ("nprcue" . (8928)) ("npre" . (10927 824)) ("nprec" . (8832)) ("npreceq" . (10927 824)) ("nrarr" . (8655)) ("nrarrc" . (10547 824)) ("nrarrw" . (8605 824)) ("nrightarrow" . (8655)) ("nrtri" . (8939)) ("nrtrie" . (8941)) ("nsc" . (8833)) ("nsccue" . (8929)) ("nsce" . (10928 824)) ("nscr" . (120003)) ("nshortmid" . (8740)) ("nshortparallel" . (8742)) ("nsim" . (8769)) ("nsime" . (8772)) ("nsimeq" . (8772)) ("nsmid" . (8740)) ("nspar" . (8742)) ("nsqsube" . (8930)) ("nsqsupe" . (8931)) ("nsub" . (8836)) ("nsube" . (10949 824)) ("nsubset" . (8834 8402)) ("nsubseteq" . (8840)) ("nsubseteqq" . (10949 824)) ("nsucc" . (8833)) ("nsucceq" . (10928 824)) ("nsup" . (8837)) ("nsupe" . (8841)) ("nsupset" . (8835 8402)) ("nsupseteq" . (8841)) ("nsupseteqq" . (10950 824)) ("ntgl" . (8825)) ("ntilde" . (241)) ("ntlg" . (8824)) ("ntriangleleft" . (8938)) ("ntrianglelefteq" . (8940)) ("ntriangleright" . (8939)) ("ntrianglerighteq" . (8941)) ("nu" . (957)) ("num" . (35)) ("numero" . (8470)) ("numsp" . (8199)) ("nvap" . (8781 8402)) ("nvdash" . (8876)) ("nvge" . (8805 8402)) ("nvgt" . (62 8402)) ("nvharr" . (10500)) ("nvinfin" . (10718)) ("nvlarr" . (10498)) ("nvle" . (8804 8402)) ("nvlt" . (60 8402)) ("nvltrie" . (8884 8402)) ("nvrarr" . (10499)) ("nvrtrie" . (8885 8402)) ("nvsim" . (8764 8402)) ("nwarhk" . (10531)) ("nwarr" . (8598)) ("nwarrow" . (8598)) ("nwnear" . (10535)) ("oacute" . (211)) ("oast" . (8859)) ("ocir" . (8858)) ("ocirc" . (212)) ("ocy" . (1054)) ("odash" . (8861)) ("odblac" . (336)) ("odiv" . (10808)) ("odot" . (8857)) ("odsold" . (10684)) ("oelig" . (338)) ("ofcir" . (10687)) ("ofr" . (120108)) ("ogon" . (731)) ("ograve" . (242)) ("ogt" . (10689)) ("ohbar" . (10677)) ("ohm" . (937)) ("oint" . (8750)) ("olarr" . (8634)) ("olcir" . (10686)) ("olcross" . (10683)) ("oline" . (8254)) ("olt" . (10688)) ("omacr" . (332)) ("omega" . (969)) ("omicron" . (927)) ("omid" . (10678)) ("ominus" . (8854)) ("oopf" . (120160)) ("opar" . (10679)) ("opencurlydoublequote" . (8220)) ("opencurlyquote" . (8216)) ("operp" . (10681)) ("oplus" . (8853)) ("or" . (10836)) ("orarr" . (8635)) ("ord" . (10845)) ("order" . (8500)) ("orderof" . (8500)) ("ordf" . (170)) ("ordm" . (186)) ("origof" . (8886)) ("oror" . (10838)) ("orslope" . (10839)) ("orv" . (10843)) ("os" . (9416)) ("oscr" . (119978)) ("oslash" . (248)) ("osol" . (8856)) ("otilde" . (213)) ("otimes" . (8855)) ("otimesas" . (10806)) ("ouml" . (246)) ("ovbar" . (9021)) ("overbar" . (8254)) ("overbrace" . (9182)) ("overbracket" . (9140)) ("overparenthesis" . (9180)) ("par" . (8741)) ("para" . (182)) ("parallel" . (8741)) ("parsim" . (10995)) ("parsl" . (11005)) ("part" . (8706)) ("partiald" . (8706)) ("pcy" . (1055)) ("percnt" . (37)) ("period" . (46)) ("permil" . (8240)) ("perp" . (8869)) ("pertenk" . (8241)) ("pfr" . (120083)) ("phi" . (934)) ("phiv" . (981)) ("phmmat" . (8499)) ("phone" . (9742)) ("pi" . (960)) ("pitchfork" . (8916)) ("piv" . (982)) ("planck" . (8463)) ("planckh" . (8462)) ("plankv" . (8463)) ("plus" . (43)) ("plusacir" . (10787)) ("plusb" . (8862)) ("pluscir" . (10786)) ("plusdo" . (8724)) ("plusdu" . (10789)) ("pluse" . (10866)) ("plusminus" . (177)) ("plusmn" . (177)) ("plussim" . (10790)) ("plustwo" . (10791)) ("pm" . (177)) ("poincareplane" . (8460)) ("pointint" . (10773)) ("popf" . (120161)) ("pound" . (163)) ("pr" . (10939)) ("prap" . (10935)) ("prcue" . (8828)) ("pre" . (10931)) ("prec" . (8826)) ("precapprox" . (10935)) ("preccurlyeq" . (8828)) ("precedes" . (8826)) ("precedesequal" . (10927)) ("precedesslantequal" . (8828)) ("precedestilde" . (8830)) ("preceq" . (10927)) ("precnapprox" . (10937)) ("precneqq" . (10933)) ("precnsim" . (8936)) ("precsim" . (8830)) ("prime" . (8242)) ("primes" . (8473)) ("prnap" . (10937)) ("prne" . (10933)) ("prnsim" . (8936)) ("prod" . (8719)) ("product" . (8719)) ("profalar" . (9006)) ("profline" . (8978)) ("profsurf" . (8979)) ("prop" . (8733)) ("proportion" . (8759)) ("proportional" . (8733)) ("propto" . (8733)) ("prsim" . (8830)) ("prurel" . (8880)) ("pscr" . (120005)) ("psi" . (936)) ("puncsp" . (8200)) ("qfr" . (120110)) ("qint" . (10764)) ("qopf" . (8474)) ("qprime" . (8279)) ("qscr" . (119980)) ("quaternions" . (8461)) ("quatint" . (10774)) ("quest" . (63)) ("questeq" . (8799)) ("quot" . (quot)) ("raarr" . (8667)) ("race" . (8765 817)) ("racute" . (340)) ("radic" . (8730)) ("raemptyv" . (10675)) ("rang" . (10219)) ("rangd" . (10642)) ("range" . (10661)) ("rangle" . (10217)) ("raquo" . (187)) ("rarr" . (8608)) ("rarrap" . (10613)) ("rarrb" . (8677)) ("rarrbfs" . (10528)) ("rarrc" . (10547)) ("rarrfs" . (10526)) ("rarrhk" . (8618)) ("rarrlp" . (8620)) ("rarrpl" . (10565)) ("rarrsim" . (10612)) ("rarrtl" . (8611)) ("rarrw" . (8605)) ("ratail" . (10524)) ("ratio" . (8758)) ("rationals" . (8474)) ("rbarr" . (10509)) ("rbbrk" . (10099)) ("rbrace" . (125)) ("rbrack" . (93)) ("rbrke" . (10636)) ("rbrksld" . (10638)) ("rbrkslu" . (10640)) ("rcaron" . (345)) ("rcedil" . (342)) ("rceil" . (8969)) ("rcub" . (125)) ("rcy" . (1088)) ("rdca" . (10551)) ("rdldhar" . (10601)) ("rdquo" . (8221)) ("rdquor" . (8221)) ("rdsh" . (8627)) ("re" . (8476)) ("real" . (8476)) ("realine" . (8475)) ("realpart" . (8476)) ("reals" . (8477)) ("rect" . (9645)) ("reg" . (174)) ("reverseelement" . (8715)) ("reverseequilibrium" . (8651)) ("reverseupequilibrium" . (10607)) ("rfisht" . (10621)) ("rfloor" . (8971)) ("rfr" . (120111)) ("rhar" . (10596)) ("rhard" . (8641)) ("rharu" . (8640)) ("rharul" . (10604)) ("rho" . (961)) ("rhov" . (1009)) ("rightanglebracket" . (10217)) ("rightarrow" . (8594)) ("rightarrowbar" . (8677)) ("rightarrowleftarrow" . (8644)) ("rightarrowtail" . (8611)) ("rightceiling" . (8969)) ("rightdoublebracket" . (10215)) ("rightdownteevector" . (10589)) ("rightdownvector" . (8642)) ("rightdownvectorbar" . (10581)) ("rightfloor" . (8971)) ("rightharpoondown" . (8641)) ("rightharpoonup" . (8640)) ("rightleftarrows" . (8644)) ("rightleftharpoons" . (8652)) ("rightrightarrows" . (8649)) ("rightsquigarrow" . (8605)) ("righttee" . (8866)) ("rightteearrow" . (8614)) ("rightteevector" . (10587)) ("rightthreetimes" . (8908)) ("righttriangle" . (8883)) ("righttrianglebar" . (10704)) ("righttriangleequal" . (8885)) ("rightupdownvector" . (10575)) ("rightupteevector" . (10588)) ("rightupvector" . (8638)) ("rightupvectorbar" . (10580)) ("rightvector" . (8640)) ("rightvectorbar" . (10579)) ("ring" . (730)) ("risingdotseq" . (8787)) ("rlarr" . (8644)) ("rlhar" . (8652)) ("rlm" . (8207)) ("rmoust" . (9137)) ("rmoustache" . (9137)) ("rnmid" . (10990)) ("roang" . (10221)) ("roarr" . (8702)) ("robrk" . (10215)) ("ropar" . (10630)) ("ropf" . (8477)) ("roplus" . (10798)) ("rotimes" . (10805)) ("roundimplies" . (10608)) ("rpar" . (41)) ("rpargt" . (10644)) ("rppolint" . (10770)) ("rrarr" . (8649)) ("rrightarrow" . (8667)) ("rsaquo" . (8250)) ("rscr" . (120007)) ("rsh" . (8625)) ("rsqb" . (93)) ("rsquo" . (8217)) ("rsquor" . (8217)) ("rthree" . (8908)) ("rtimes" . (8906)) ("rtri" . (9657)) ("rtrie" . (8885)) ("rtrif" . (9656)) ("rtriltri" . (10702)) ("ruledelayed" . (10740)) ("ruluhar" . (10600)) ("rx" . (8478)) ("sacute" . (347)) ("sbquo" . (8218)) ("sc" . (10940)) ("scap" . (10936)) ("scaron" . (352)) ("sccue" . (8829)) ("sce" . (10928)) ("scedil" . (350)) ("scirc" . (348)) ("scnap" . (10938)) ("scne" . (10934)) ("scnsim" . (8937)) ("scpolint" . (10771)) ("scsim" . (8831)) ("scy" . (1057)) ("sdot" . (8901)) ("sdotb" . (8865)) ("sdote" . (10854)) ("searhk" . (10533)) ("searr" . (8600)) ("searrow" . (8600)) ("sect" . (167)) ("semi" . (59)) ("seswar" . (10537)) ("setminus" . (8726)) ("setmn" . (8726)) ("sext" . (10038)) ("sfr" . (120112)) ("sfrown" . (8994)) ("sharp" . (9839)) ("shchcy" . (1097)) ("shcy" . (1096)) ("shortdownarrow" . (8595)) ("shortleftarrow" . (8592)) ("shortmid" . (8739)) ("shortparallel" . (8741)) ("shortrightarrow" . (8594)) ("shortuparrow" . (8593)) ("shy" . (173)) ("sigma" . (931)) ("sigmaf" . (962)) ("sigmav" . (962)) ("sim" . (8764)) ("simdot" . (10858)) ("sime" . (8771)) ("simeq" . (8771)) ("simg" . (10910)) ("simge" . (10912)) ("siml" . (10909)) ("simle" . (10911)) ("simne" . (8774)) ("simplus" . (10788)) ("simrarr" . (10610)) ("slarr" . (8592)) ("smallcircle" . (8728)) ("smallsetminus" . (8726)) ("smashp" . (10803)) ("smeparsl" . (10724)) ("smid" . (8739)) ("smile" . (8995)) ("smt" . (10922)) ("smte" . (10924)) ("smtes" . (10924 65024)) ("softcy" . (1100)) ("sol" . (47)) ("solb" . (10692)) ("solbar" . (9023)) ("sopf" . (120164)) ("spades" . (9824)) ("spadesuit" . (9824)) ("spar" . (8741)) ("sqcap" . (8851)) ("sqcaps" . (8851 65024)) ("sqcup" . (8852)) ("sqcups" . (8852 65024)) ("sqrt" . (8730)) ("sqsub" . (8847)) ("sqsube" . (8849)) ("sqsubset" . (8847)) ("sqsubseteq" . (8849)) ("sqsup" . (8848)) ("sqsupe" . (8850)) ("sqsupset" . (8848)) ("sqsupseteq" . (8850)) ("squ" . (9633)) ("square" . (9633)) ("squareintersection" . (8851)) ("squaresubset" . (8847)) ("squaresubsetequal" . (8849)) ("squaresuperset" . (8848)) ("squaresupersetequal" . (8850)) ("squareunion" . (8852)) ("squarf" . (9642)) ("squf" . (9642)) ("srarr" . (8594)) ("sscr" . (119982)) ("ssetmn" . (8726)) ("ssmile" . (8995)) ("sstarf" . (8902)) ("star" . (8902)) ("starf" . (9733)) ("straightepsilon" . (1013)) ("straightphi" . (981)) ("strns" . (175)) ("sub" . (8834)) ("subdot" . (10941)) ("sube" . (10949)) ("subedot" . (10947)) ("submult" . (10945)) ("subne" . (8842)) ("subplus" . (10943)) ("subrarr" . (10617)) ("subset" . (8912)) ("subseteq" . (8838)) ("subseteqq" . (10949)) ("subsetequal" . (8838)) ("subsetneq" . (8842)) ("subsetneqq" . (10955)) ("subsim" . (10951)) ("subsub" . (10965)) ("subsup" . (10963)) ("succ" . (8827)) ("succapprox" . (10936)) ("succcurlyeq" . (8829)) ("succeeds" . (8827)) ("succeedsequal" . (10928)) ("succeedsslantequal" . (8829)) ("succeedstilde" . (8831)) ("succeq" . (10928)) ("succnapprox" . (10938)) ("succneqq" . (10934)) ("succnsim" . (8937)) ("succsim" . (8831)) ("suchthat" . (8715)) ("sum" . (8721)) ("sung" . (9834)) ("sup" . (8913)) ("sup1" . (185)) ("sup2" . (178)) ("sup3" . (179)) ("supdot" . (10942)) ("supdsub" . (10968)) ("supe" . (8839)) ("supedot" . (10948)) ("superset" . (8835)) ("supersetequal" . (8839)) ("suphsol" . (10185)) ("suphsub" . (10967)) ("suplarr" . (10619)) ("supmult" . (10946)) ("supne" . (10956)) ("supplus" . (10944)) ("supset" . (8913)) ("supseteq" . (8839)) ("supseteqq" . (10950)) ("supsetneq" . (8843)) ("supsetneqq" . (10956)) ("supsim" . (10952)) ("supsub" . (10964)) ("supsup" . (10966)) ("swarhk" . (10534)) ("swarr" . (8665)) ("swarrow" . (8601)) ("swnwar" . (10538)) ("szlig" . (223)) ("tab" . (9)) ("target" . (8982)) ("tau" . (932)) ("tbrk" . (9140)) ("tcaron" . (356)) ("tcedil" . (355)) ("tcy" . (1090)) ("tdot" . (8411)) ("telrec" . (8981)) ("tfr" . (120087)) ("there4" . (8756)) ("therefore" . (8756)) ("theta" . (920)) ("thetasym" . (977)) ("thetav" . (977)) ("thickapprox" . (8776)) ("thicksim" . (8764)) ("thickspace" . (8287 8202)) ("thinsp" . (8201)) ("thinspace" . (8201)) ("thkap" . (8776)) ("thksim" . (8764)) ("thorn" . (222)) ("tilde" . (732)) ("tildeequal" . (8771)) ("tildefullequal" . (8773)) ("tildetilde" . (8776)) ("times" . (215)) ("timesb" . (8864)) ("timesbar" . (10801)) ("timesd" . (10800)) ("tint" . (8749)) ("toea" . (10536)) ("top" . (8868)) ("topbot" . (9014)) ("topcir" . (10993)) ("topf" . (120139)) ("topfork" . (10970)) ("tosa" . (10537)) ("tprime" . (8244)) ("trade" . (8482)) ("triangle" . (9653)) ("triangledown" . (9663)) ("triangleleft" . (9667)) ("trianglelefteq" . (8884)) ("triangleq" . (8796)) ("triangleright" . (9657)) ("trianglerighteq" . (8885)) ("tridot" . (9708)) ("trie" . (8796)) ("triminus" . (10810)) ("tripledot" . (8411)) ("triplus" . (10809)) ("trisb" . (10701)) ("tritime" . (10811)) ("trpezium" . (9186)) ("tscr" . (119983)) ("tscy" . (1094)) ("tshcy" . (1035)) ("tstrok" . (358)) ("twixt" . (8812)) ("twoheadleftarrow" . (8606)) ("twoheadrightarrow" . (8608)) ("uacute" . (218)) ("uarr" . (8607)) ("uarrocir" . (10569)) ("ubrcy" . (1038)) ("ubreve" . (365)) ("ucirc" . (219)) ("ucy" . (1091)) ("udarr" . (8645)) ("udblac" . (369)) ("udhar" . (10606)) ("ufisht" . (10622)) ("ufr" . (120088)) ("ugrave" . (249)) ("uhar" . (10595)) ("uharl" . (8639)) ("uharr" . (8638)) ("uhblk" . (9600)) ("ulcorn" . (8988)) ("ulcorner" . (8988)) ("ulcrop" . (8975)) ("ultri" . (9720)) ("umacr" . (363)) ("uml" . (168)) ("underbar" . (95)) ("underbrace" . (9183)) ("underbracket" . (9141)) ("underparenthesis" . (9181)) ("union" . (8899)) ("unionplus" . (8846)) ("uogon" . (370)) ("uopf" . (120140)) ("uparrow" . (8593)) ("uparrowbar" . (10514)) ("uparrowdownarrow" . (8645)) ("updownarrow" . (8597)) ("upequilibrium" . (10606)) ("upharpoonleft" . (8639)) ("upharpoonright" . (8638)) ("uplus" . (8846)) ("upperleftarrow" . (8598)) ("upperrightarrow" . (8599)) ("upsi" . (965)) ("upsih" . (978)) ("upsilon" . (933)) ("uptee" . (8869)) ("upteearrow" . (8613)) ("upuparrows" . (8648)) ("urcorn" . (8989)) ("urcorner" . (8989)) ("urcrop" . (8974)) ("uring" . (366)) ("urtri" . (9721)) ("uscr" . (120010)) ("utdot" . (8944)) ("utilde" . (360)) ("utri" . (9653)) ("utrif" . (9652)) ("uuarr" . (8648)) ("uuml" . (220)) ("uwangle" . (10663)) ("vangrt" . (10652)) ("varepsilon" . (1013)) ("varkappa" . (1008)) ("varnothing" . (8709)) ("varphi" . (981)) ("varpi" . (982)) ("varpropto" . (8733)) ("varr" . (8597)) ("varrho" . (1009)) ("varsigma" . (962)) ("varsubsetneq" . (8842 65024)) ("varsubsetneqq" . (10955 65024)) ("varsupsetneq" . (8843 65024)) ("varsupsetneqq" . (10956 65024)) ("vartheta" . (977)) ("vartriangleleft" . (8882)) ("vartriangleright" . (8883)) ("vbar" . (10984)) ("vbarv" . (10985)) ("vcy" . (1074)) ("vdash" . (8866)) ("vdashl" . (10982)) ("vee" . (8744)) ("veebar" . (8891)) ("veeeq" . (8794)) ("vellip" . (8942)) ("verbar" . (124)) ("vert" . (124)) ("verticalbar" . (8739)) ("verticalline" . (124)) ("verticalseparator" . (10072)) ("verticaltilde" . (8768)) ("verythinspace" . (8202)) ("vfr" . (120115)) ("vltri" . (8882)) ("vnsub" . (8834 8402)) ("vnsup" . (8835 8402)) ("vopf" . (120141)) ("vprop" . (8733)) ("vrtri" . (8883)) ("vscr" . (120011)) ("vsubne" . (10955 65024)) ("vsupne" . (8843 65024)) ("vvdash" . (8874)) ("vzigzag" . (10650)) ("wcirc" . (373)) ("wedbar" . (10847)) ("wedge" . (8743)) ("wedgeq" . (8793)) ("weierp" . (8472)) ("wfr" . (120090)) ("wopf" . (120142)) ("wp" . (8472)) ("wr" . (8768)) ("wreath" . (8768)) ("wscr" . (120012)) ("xcap" . (8898)) ("xcirc" . (9711)) ("xcup" . (8899)) ("xdtri" . (9661)) ("xfr" . (120091)) ("xharr" . (10231)) ("xi" . (926)) ("xlarr" . (10232)) ("xmap" . (10236)) ("xnis" . (8955)) ("xodot" . (10752)) ("xopf" . (120169)) ("xoplus" . (10753)) ("xotime" . (10754)) ("xrarr" . (10230)) ("xscr" . (120013)) ("xsqcup" . (10758)) ("xuplus" . (10756)) ("xutri" . (9651)) ("xvee" . (8897)) ("xwedge" . (8896)) ("yacute" . (253)) ("yacy" . (1071)) ("ycirc" . (375)) ("ycy" . (1099)) ("yen" . (165)) ("yfr" . (120092)) ("yicy" . (1031)) ("yopf" . (120170)) ("yscr" . (119988)) ("yucy" . (1070)) ("yuml" . (255)) ("zacute" . (377)) ("zcaron" . (382)) ("zcy" . (1079)) ("zdot" . (379)) ("zeetrf" . (8488)) ("zerowidthspace" . (8203)) ("zeta" . (950)) ("zfr" . (8488)) ("zhcy" . (1078)) ("zigrarr" . (8669)) ("zopf" . (8484)) ("zscr" . (120015)) ("zwj" . (8205)) ("zwnj" . (8204))) -------------------------------------------------------------------------------- /splitflap-lib/private/feed.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | ;; ~~ RSS Feed Generation ~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | (require gregor 6 | (only-in net/url 7 | url?) 8 | racket/contract 9 | racket/generic 10 | racket/list 11 | racket/match 12 | racket/promise 13 | (only-in racket/string 14 | non-empty-string?) 15 | xml 16 | "dust.rkt" 17 | "validation.rkt" 18 | "version.rkt" 19 | "xml-generic.rkt") 20 | 21 | (provide food? 22 | (contract-out 23 | [feed-xslt-stylesheet (parameter/c (or/c non-empty-string? url? #f))] 24 | [feed-language (parameter/c (or/c iso-639-language-code? #f))] 25 | [rename make-feed-item feed-item 26 | (->* (tag-uri? valid-url-string? string? person? moment? moment? xexpr?) 27 | ((or/c enclosure? #f)) 28 | feed-item?)] 29 | [rename make-feed feed (-> tag-uri? valid-url-string? string? (listof feed-item?) feed?)] 30 | [rename make-episode episode 31 | (->* (tag-uri? valid-url-string? string? person? moment? moment? xexpr? enclosure?) 32 | (#:duration (or/c exact-nonnegative-integer? #f) 33 | #:image-url (or/c valid-url-string? #f) 34 | #:explicit? any/c 35 | #:episode-num (or/c exact-nonnegative-integer? #f) 36 | #:season-num (or/c exact-nonnegative-integer? #f) 37 | #:type (or/c 'trailer 'full 'bonus #f) 38 | #:block? any/c) 39 | episode?)] 40 | [rename make-podcast podcast 41 | (->* (tag-uri? 42 | valid-url-string? 43 | string? 44 | (listof episode?) 45 | (or/c string? (list/c string? string?)) 46 | valid-url-string? 47 | person? 48 | #:explicit? any/c) 49 | (#:type (or/c 'serial 'episodic #f) 50 | #:block? any/c 51 | #:complete? any/c 52 | #:new-feed-url (or/c valid-url-string? #f)) 53 | podcast?)]) 54 | feed-item? 55 | episode? 56 | feed? 57 | podcast? 58 | include-generator? 59 | express-xml) 60 | 61 | ;; ~~ Ancillary elements ~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | 63 | (define feed-language (make-parameter #f)) 64 | 65 | ;; Build the tag (caches for each dialect) 66 | (define generator 67 | (let ([cache (make-hash)]) 68 | (λ (dialect) 69 | (hash-ref! 70 | cache 71 | dialect 72 | (λ () 73 | (define gen-str (format "Racket v~a [~a] + splitflap v~a" (version) (system-type 'gc) (splitflap-version))) 74 | (case dialect 75 | [(rss) `(generator ,(string-append gen-str " (https://racket-lang.org)"))] 76 | [(atom) `(generator [[uri "https://racket-lang.org"] [version ,(version)]] ,gen-str)])))))) 77 | 78 | ;; Parameter determines whether feed output will include a tag 79 | ;; Useful for keeping unit tests from breaking on different versions 80 | (define include-generator? (make-parameter #t)) 81 | 82 | ;; 83 | ;; ~~ Feed Entries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 84 | 85 | (struct feed-item 86 | (id url title author published updated content media) 87 | #:constructor-name feed-item_ 88 | #:methods gen:food 89 | [(define/generic <-express-xml express-xml) 90 | (define/contract (express-xml e dialect [url #f] #:as [result-type 'xml-string]) 91 | (->* (any/c rss-dialect?) (any/c #:as xml-type/c) any/c) 92 | (match-define (feed-item id url title author published updated raw-content media) e) 93 | (define to-xml? (memq result-type '(xml xml-string xexpr-cdata))) 94 | (define entry-xpr 95 | (case dialect 96 | [(atom) 97 | `(entry 98 | (title [[type "text"]] ,title) 99 | (link [[rel "alternate"] [href ,url]]) 100 | (updated ,(moment->string updated 'atom)) 101 | (published ,(moment->string published 'atom)) 102 | ,@(if/sp media (<-express-xml media 'atom #:as 'xexpr)) 103 | ,(person->xexpr author 'author 'atom) 104 | (id ,(tag-uri->string id)) 105 | ,(content->safe-element raw-content 'content 'atom to-xml?))] 106 | [(rss) 107 | `(item 108 | (title ,title) 109 | (link ,url) 110 | (pubDate ,(moment->string published 'rss)) 111 | ,@(if/sp media (<-express-xml media 'rss #:as 'xexpr)) 112 | ,(person->xexpr author 'author 'rss) 113 | (guid [[isPermaLink "false"]] ,(tag-uri->string id)) 114 | ,(content->safe-element raw-content 'description 'rss to-xml?))])) 115 | (case result-type 116 | [(xexpr xexpr-cdata) entry-xpr] 117 | [(xml) (xexpr->xml entry-xpr)] 118 | [(xml-string) (indented-xml-string entry-xpr)]))]) 119 | 120 | (define (make-feed-item id url title author published updated content [media #f]) 121 | (unless (moment>=? updated published) 122 | (raise-arguments-error 'feed-item "updated timestamp cannot come before published timestamp" 123 | "updated" updated "published" published)) 124 | (feed-item_ id url title author published updated content media)) 125 | 126 | (define (entry-newer? maybe-newer other) 127 | (moment>? (feed-item-updated maybe-newer) (feed-item-updated other))) 128 | 129 | ;; 130 | ;; ~~ Feeds ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 131 | 132 | (struct feed 133 | (id site-url name entries) 134 | #:guard (struct-guard/c tag-uri? valid-url-string? string? (listof feed-item?)) 135 | #:constructor-name feed_ 136 | #:methods gen:food 137 | [(define/generic <-express-xml express-xml) 138 | (define/contract (express-xml f dialect [feed-url #f] #:as [result-type 'xml-string]) 139 | (->* (any/c rss-dialect?) ((or/c valid-url-string? #f) #:as xml-type/c) (or/c string? document? txexpr?)) 140 | (unless (valid-url-string? feed-url) 141 | (raise-argument-error 'express-xml "valid URL (required for #)" feed-url)) 142 | (match-define (feed feed-id site-url feed-name entries) f) 143 | (define entries-sorted (sort entries entry-newer?)) 144 | (define last-updated (if (pair? entries-sorted) (feed-item-updated (car entries-sorted)) (infer-moment))) 145 | (define to-xml? (memq result-type '(xml xml-string))) 146 | 147 | (define feed-xpr 148 | (case dialect 149 | [(atom) 150 | `(feed [[xmlns "http://www.w3.org/2005/Atom"] 151 | [xml:lang ,(symbol->string (or (feed-language) (force system-language)))]] 152 | (title [[type "text"]] ,feed-name) 153 | (link [[rel "self"] [href ,feed-url]]) 154 | (link [[rel "alternate"] [href ,site-url]]) 155 | (updated ,(moment->string last-updated 'atom)) 156 | (id ,(tag-uri->string feed-id)) 157 | ,@(if/sp (include-generator?) (generator 'atom)) 158 | ,@(for/list ([e (in-list entries-sorted)]) 159 | (<-express-xml e 'atom #:as (if to-xml? 'xexpr-cdata 'xexpr))))] 160 | [(rss) 161 | `(rss [[version "2.0"] [xmlns:atom "http://www.w3.org/2005/Atom"]] 162 | (channel 163 | (title ,feed-name) 164 | (atom:link [[rel "self"] [href ,feed-url] [type "application/rss+xml"]]) 165 | (link ,site-url) 166 | (pubDate ,(moment->string last-updated 'rss)) 167 | (lastBuildDate ,(moment->string last-updated 'rss)) 168 | ,@(if/sp (include-generator?) (generator 'rss)) 169 | (description ,feed-name) 170 | (language ,(symbol->string (or (feed-language) (force system-language)))) 171 | ,@(for/list ([e (in-list entries-sorted)]) 172 | (<-express-xml e 'rss #:as (if to-xml? 'xexpr-cdata 'xexpr)))))])) 173 | (case result-type 174 | [(xexpr xexpr-cdata) feed-xpr] 175 | [(xml) (xml-document feed-xpr)] 176 | [(xml-string) (indented-xml-string feed-xpr #:document? #t)]))]) 177 | 178 | (define (make-feed id site-url name entries) 179 | (let* ([entry-tags (map feed-item-id entries)] 180 | [duplicate-tag (check-duplicates (cons id entry-tags) tag=?)]) 181 | (when duplicate-tag 182 | (raise-arguments-error 'feed 183 | "Duplicate tag URI found in feed" 184 | "First duplicate encountered" (tag-uri->string duplicate-tag) 185 | "Feed tag URI" (tag-uri->string id) 186 | "Feed item tag URIs" (map tag-uri->string entry-tags)))) 187 | (feed_ id site-url name entries)) 188 | 189 | ;; ~~ Podcast episodes and feeds ~~~~~~~~~~~~~~~~~ 190 | 191 | ;; Episodes are feed-entries that require an enclosure, and add required and optional tags 192 | ;; and flags from Apple’s podcast feed specifications. 193 | (struct episode (id url title author published updated content media 194 | duration image-url explicit? episode-n season-n type block?) 195 | #:constructor-name episode_ 196 | #:methods gen:food 197 | [(define/generic <-express-xml express-xml) 198 | (define/contract (express-xml ep dialect [feed-url #f] #:as [result-type 'xml-string]) 199 | (->* (any/c rss-dialect?) (any/c #:as xml-type/c) any/c) 200 | (define to-xml? (memq result-type '(xml xml-string xexpr-cdata))) 201 | (match-define 202 | (episode id url title author published updated content media 203 | duration image-url explicit? episode-n season-n ep-type block?) ep) 204 | (define episode-xpr 205 | `(item 206 | (title ,title) 207 | (link ,url) 208 | (pubDate ,(moment->string updated 'rss)) 209 | ,@(if/sp media (<-express-xml media 'rss #:as 'xexpr)) 210 | ,@(if/sp ep-type `(itunes:episodeType ,(symbol->string ep-type))) 211 | ,(person->xexpr author 'author 'rss) 212 | (guid [[isPermaLink "false"]] ,(tag-uri->string id)) 213 | ,(content->safe-element content 'description 'rss to-xml?) 214 | ,@(if/sp duration `(itunes:duration ,(number->string duration))) 215 | ,@(if/sp (not (null? explicit?)) `(itunes:explicit ,(if explicit? "true" "false"))) 216 | ,@(if/sp image-url `(itunes:image [[href ,image-url]])) 217 | ,@(if/sp episode-n `(itunes:episode ,(number->string episode-n))) 218 | ,@(if/sp season-n `(itunes:season ,(number->string season-n))) 219 | ,@(if/sp block? '(itunes:block "Yes")) 220 | )) 221 | (case result-type 222 | [(xexpr xexpr-cdata) episode-xpr] 223 | [(xml) (xexpr->xml episode-xpr)] 224 | [(xml-string) (indented-xml-string episode-xpr)]))]) 225 | 226 | (define (make-episode id url title author published updated content media 227 | #:duration [duration #f] 228 | #:image-url [image-url #f] 229 | #:explicit? [explicit? null] 230 | #:episode-num [episode-n #f] 231 | #:season-num [season-n #f] 232 | #:type [type #f] 233 | #:block? [block? #f]) 234 | (unless (moment>=? updated published) 235 | (raise-arguments-error 'feed-item "updated timestamp cannot come before published timestamp" 236 | "updated" updated "published" published)) 237 | (episode_ id url title author published updated content media 238 | duration image-url explicit? episode-n season-n type block?)) 239 | 240 | ;; Podcasts are feeds with more requirements. They will only ever express as RSS 2.0 because that’s 241 | ;; what Apple’s podcast directory requires. 242 | (struct podcast (id site-url name entries category image-url owner explicit? type block? complete? new-feed-url) 243 | #:constructor-name podcast_ 244 | #:methods gen:food 245 | [(define/generic <-express-xml express-xml) 246 | (define/contract (express-xml p dialect [feed-url #f] #:as [result-type 'xml-string]) 247 | (->* (any/c rss-dialect?) ((or/c valid-url-string? #f) #:as xml-type/c) (or/c string? document? txexpr?)) 248 | (unless (valid-url-string? feed-url) 249 | (raise-argument-error 'express-xml "valid URL (required for #)" feed-url)) 250 | (match-define (podcast feed-id site-url feed-name episodes cat image-url owner explicit? type block? complete? new-feed-url) p) 251 | (define episodes-sorted (sort episodes entry-newer?)) 252 | (define category 253 | (match cat 254 | [(list cat1 cat2) 255 | `(itunes:category [[text ,cat1]] 256 | (itunes:category [[text ,cat2]]))] 257 | [(? string? c) `(itunes:category [[text ,c]])])) 258 | (define last-updated (if (pair? episodes-sorted) (episode-updated (car episodes-sorted)) (infer-moment))) 259 | (define to-xml? (memq result-type '(xml xml-string))) 260 | 261 | (define feed-xpr 262 | `(rss [[version "2.0"] 263 | [xmlns:atom "http://www.w3.org/2005/Atom"] 264 | [xmlns:itunes "http://www.itunes.com/dtds/podcast-1.0.dtd"]] 265 | (channel 266 | (title ,feed-name) 267 | (atom:link [[rel "self"] [href ,feed-url] [type "application/rss+xml"]]) 268 | (link ,site-url) 269 | (pubDate ,(moment->string last-updated 'rss)) 270 | (lastBuildDate ,(moment->string last-updated 'rss)) 271 | ,@(if/sp (include-generator?) (generator 'rss)) 272 | (description ,feed-name) 273 | (language ,(symbol->string (or (feed-language) (force system-language)))) 274 | ,(person->xexpr owner 'itunes:owner 'itunes) 275 | (itunes:image [[href ,image-url]]) 276 | ,category 277 | (itunes:explicit ,(if explicit? "yes" "no")) 278 | ,@(if/sp type `(itunes:type ,(symbol->string type))) 279 | ,@(if/sp block? '(itunes:block "Yes")) 280 | ,@(if/sp complete? '(itunes:complete "Yes")) 281 | ,@(if/sp new-feed-url `(itunes:new-feed-url ,new-feed-url)) 282 | ,@(for/list ([e (in-list episodes-sorted)]) 283 | (<-express-xml e 'rss #:as (if to-xml? 'xexpr-cdata 'xexpr)))))) 284 | (case result-type 285 | [(xexpr xexpr-cdata) feed-xpr] 286 | [(xml) (xml-document feed-xpr)] 287 | [(xml-string) (indented-xml-string feed-xpr #:document? #t)]))]) 288 | 289 | (define (make-podcast id site-url name episodes category image-url owner 290 | #:explicit? explicit? 291 | #:type [type #f] 292 | #:block? [block? #f] 293 | #:complete? [complete? #f] 294 | #:new-feed-url [new-feed-url #f]) 295 | (let* ([episode-tags (map episode-id episodes)] 296 | [duplicate-tag (check-duplicates (cons id episode-tags) tag=?)]) 297 | (when duplicate-tag 298 | (raise-arguments-error 'podcast 299 | "Duplicate tag URI found in podcast feed" 300 | "First duplicate encountered" (tag-uri->string duplicate-tag) 301 | "Feed tag URI" (tag-uri->string id) 302 | "Episode tag URIs" (map tag-uri->string episode-tags)))) 303 | (podcast_ id site-url name episodes category image-url owner explicit? type block? complete? new-feed-url)) 304 | -------------------------------------------------------------------------------- /splitflap-lib/private/mime-types.rktd: -------------------------------------------------------------------------------- 1 | #hasheq((|123| . "application/vnd.lotus-1-2-3") (3dml . "text/vnd.in3d.3dml") (3ds . "image/x-3ds") (3g2 . "video/3gpp2") (3gp . "video/3gpp") (7z . "application/x-7z-compressed") (aab . "application/x-authorware-bin") (aac . "audio/x-aac") (aam . "application/x-authorware-map") (aas . "application/x-authorware-seg") (abw . "application/x-abiword") (ac . "application/pkix-attr-cert") (acc . "application/vnd.americandynamics.acc") (ace . "application/x-ace-compressed") (acu . "application/vnd.acucobol") (acutc . "application/vnd.acucorp") (adp . "audio/adpcm") (aep . "application/vnd.audiograph") (afm . "application/x-font-type1") (afp . "application/vnd.ibm.modcap") (ahead . "application/vnd.ahead.space") (ai . "application/postscript") (aif . "audio/x-aiff") (aifc . "audio/x-aiff") (aiff . "audio/x-aiff") (air . "application/vnd.adobe.air-application-installer-package+zip") (ait . "application/vnd.dvb.ait") (ami . "application/vnd.amiga.ami") (apk . "application/vnd.android.package-archive") (appcache . "text/cache-manifest") (application . "application/x-ms-application") (apr . "application/vnd.lotus-approach") (arc . "application/x-freearc") (asc . "application/pgp-signature") (asf . "video/x-ms-asf") (asm . "text/x-asm") (aso . "application/vnd.accpac.simply.aso") (asx . "video/x-ms-asf") (atc . "application/vnd.acucorp") (atom . "application/atom+xml") (atomcat . "application/atomcat+xml") (atomsvc . "application/atomsvc+xml") (atx . "application/vnd.antix.game-component") (au . "audio/basic") (avi . "video/x-msvideo") (avif . "image/avif") (aw . "application/applixware") (azf . "application/vnd.airzip.filesecure.azf") (azs . "application/vnd.airzip.filesecure.azs") (azw . "application/vnd.amazon.ebook") (bat . "application/x-msdownload") (bcpio . "application/x-bcpio") (bdf . "application/x-font-bdf") (bdm . "application/vnd.syncml.dm+wbxml") (bed . "application/vnd.realvnc.bed") (bh2 . "application/vnd.fujitsu.oasysprs") (bin . "application/octet-stream") (blb . "application/x-blorb") (blorb . "application/x-blorb") (bmi . "application/vnd.bmi") (bmp . "image/bmp") (book . "application/vnd.framemaker") (box . "application/vnd.previewsystems.box") (boz . "application/x-bzip2") (bpk . "application/octet-stream") (btif . "image/prs.btif") (bz . "application/x-bzip") (bz2 . "application/x-bzip2") (c . "text/x-c") (c11amc . "application/vnd.cluetrust.cartomobile-config") (c11amz . "application/vnd.cluetrust.cartomobile-config-pkg") (c4d . "application/vnd.clonk.c4group") (c4f . "application/vnd.clonk.c4group") (c4g . "application/vnd.clonk.c4group") (c4p . "application/vnd.clonk.c4group") (c4u . "application/vnd.clonk.c4group") (cab . "application/vnd.ms-cab-compressed") (caf . "audio/x-caf") (cap . "application/vnd.tcpdump.pcap") (car . "application/vnd.curl.car") (cat . "application/vnd.ms-pki.seccat") (cb7 . "application/x-cbr") (cba . "application/x-cbr") (cbr . "application/x-cbr") (cbt . "application/x-cbr") (cbz . "application/x-cbr") (cc . "text/x-c") (cct . "application/x-director") (ccxml . "application/ccxml+xml") (cdbcmsg . "application/vnd.contact.cmsg") (cdf . "application/x-netcdf") (cdkey . "application/vnd.mediastation.cdkey") (cdmia . "application/cdmi-capability") (cdmic . "application/cdmi-container") (cdmid . "application/cdmi-domain") (cdmio . "application/cdmi-object") (cdmiq . "application/cdmi-queue") (cdx . "chemical/x-cdx") (cdxml . "application/vnd.chemdraw+xml") (cdy . "application/vnd.cinderella") (cer . "application/pkix-cert") (cfs . "application/x-cfs-compressed") (cgm . "image/cgm") (chat . "application/x-chat") (chm . "application/vnd.ms-htmlhelp") (chrt . "application/vnd.kde.kchart") (cif . "chemical/x-cif") (cii . "application/vnd.anser-web-certificate-issue-initiation") (cil . "application/vnd.ms-artgalry") (cla . "application/vnd.claymore") (class . "application/java-vm") (clkk . "application/vnd.crick.clicker.keyboard") (clkp . "application/vnd.crick.clicker.palette") (clkt . "application/vnd.crick.clicker.template") (clkw . "application/vnd.crick.clicker.wordbank") (clkx . "application/vnd.crick.clicker") (clp . "application/x-msclip") (cmc . "application/vnd.cosmocaller") (cmdf . "chemical/x-cmdf") (cml . "chemical/x-cml") (cmp . "application/vnd.yellowriver-custom-menu") (cmx . "image/x-cmx") (cod . "application/vnd.rim.cod") (com . "application/x-msdownload") (conf . "text/plain") (cpio . "application/x-cpio") (cpp . "text/x-c") (cpt . "application/mac-compactpro") (crd . "application/x-mscardfile") (crl . "application/pkix-crl") (crt . "application/x-x509-ca-cert") (cryptonote . "application/vnd.rig.cryptonote") (csh . "application/x-csh") (csml . "chemical/x-csml") (csp . "application/vnd.commonspace") (css . "text/css") (cst . "application/x-director") (csv . "text/csv") (cu . "application/cu-seeme") (curl . "text/vnd.curl") (cww . "application/prs.cww") (cxt . "application/x-director") (cxx . "text/x-c") (dae . "model/vnd.collada+xml") (daf . "application/vnd.mobius.daf") (dart . "application/vnd.dart") (dataless . "application/vnd.fdsn.seed") (davmount . "application/davmount+xml") (dbk . "application/docbook+xml") (dcr . "application/x-director") (dcurl . "text/vnd.curl.dcurl") (dd2 . "application/vnd.oma.dd2+xml") (ddd . "application/vnd.fujixerox.ddd") (deb . "application/x-debian-package") (def . "text/plain") (deploy . "application/octet-stream") (der . "application/x-x509-ca-cert") (dfac . "application/vnd.dreamfactory") (dgc . "application/x-dgc-compressed") (dic . "text/x-c") (dir . "application/x-director") (dis . "application/vnd.mobius.dis") (dist . "application/octet-stream") (distz . "application/octet-stream") (djv . "image/vnd.djvu") (djvu . "image/vnd.djvu") (dll . "application/x-msdownload") (dmg . "application/x-apple-diskimage") (dmp . "application/vnd.tcpdump.pcap") (dms . "application/octet-stream") (dna . "application/vnd.dna") (doc . "application/msword") (docm . "application/vnd.ms-word.document.macroenabled.12") (docx . "application/vnd.openxmlformats-officedocument.wordprocessingml.document") (dot . "application/msword") (dotm . "application/vnd.ms-word.template.macroenabled.12") (dotx . "application/vnd.openxmlformats-officedocument.wordprocessingml.template") (dp . "application/vnd.osgi.dp") (dpg . "application/vnd.dpgraph") (dra . "audio/vnd.dra") (dsc . "text/prs.lines.tag") (dssc . "application/dssc+der") (dtb . "application/x-dtbook+xml") (dtd . "application/xml-dtd") (dts . "audio/vnd.dts") (dtshd . "audio/vnd.dts.hd") (dump . "application/octet-stream") (dvb . "video/vnd.dvb.file") (dvi . "application/x-dvi") (dwf . "model/vnd.dwf") (dwg . "image/vnd.dwg") (dxf . "image/vnd.dxf") (dxp . "application/vnd.spotfire.dxp") (dxr . "application/x-director") (ecelp4800 . "audio/vnd.nuera.ecelp4800") (ecelp7470 . "audio/vnd.nuera.ecelp7470") (ecelp9600 . "audio/vnd.nuera.ecelp9600") (ecma . "application/ecmascript") (edm . "application/vnd.novadigm.edm") (edx . "application/vnd.novadigm.edx") (efif . "application/vnd.picsel") (ei6 . "application/vnd.pg.osasli") (elc . "application/octet-stream") (emf . "application/x-msmetafile") (eml . "message/rfc822") (emma . "application/emma+xml") (emz . "application/x-msmetafile") (eol . "audio/vnd.digital-winds") (eot . "application/vnd.ms-fontobject") (eps . "application/postscript") (epub . "application/epub+zip") (es3 . "application/vnd.eszigno3+xml") (esa . "application/vnd.osgi.subsystem") (esf . "application/vnd.epson.esf") (et3 . "application/vnd.eszigno3+xml") (etx . "text/x-setext") (eva . "application/x-eva") (evy . "application/x-envoy") (exe . "application/x-msdownload") (exi . "application/exi") (ext . "application/vnd.novadigm.ext") (ez . "application/andrew-inset") (ez2 . "application/vnd.ezpix-album") (ez3 . "application/vnd.ezpix-package") (f . "text/x-fortran") (f4v . "video/x-f4v") (f77 . "text/x-fortran") (f90 . "text/x-fortran") (fbs . "image/vnd.fastbidsheet") (fcdt . "application/vnd.adobe.formscentral.fcdt") (fcs . "application/vnd.isac.fcs") (fdf . "application/vnd.fdf") (fe_launch . "application/vnd.denovo.fcselayout-link") (fg5 . "application/vnd.fujitsu.oasysgp") (fgd . "application/x-director") (fh . "image/x-freehand") (fh4 . "image/x-freehand") (fh5 . "image/x-freehand") (fh7 . "image/x-freehand") (fhc . "image/x-freehand") (fig . "application/x-xfig") (flac . "audio/x-flac") (fli . "video/x-fli") (flo . "application/vnd.micrografx.flo") (flv . "video/x-flv") (flw . "application/vnd.kde.kivio") (flx . "text/vnd.fmi.flexstor") (fly . "text/vnd.fly") (fm . "application/vnd.framemaker") (fnc . "application/vnd.frogans.fnc") (for . "text/x-fortran") (fpx . "image/vnd.fpx") (frame . "application/vnd.framemaker") (fsc . "application/vnd.fsc.weblaunch") (fst . "image/vnd.fst") (ftc . "application/vnd.fluxtime.clip") (fti . "application/vnd.anser-web-funds-transfer-initiation") (fvt . "video/vnd.fvt") (fxp . "application/vnd.adobe.fxp") (fxpl . "application/vnd.adobe.fxp") (fzs . "application/vnd.fuzzysheet") (g2w . "application/vnd.geoplan") (g3 . "image/g3fax") (g3w . "application/vnd.geospace") (gac . "application/vnd.groove-account") (gam . "application/x-tads") (gbr . "application/rpki-ghostbusters") (gca . "application/x-gca-compressed") (gdl . "model/vnd.gdl") (geo . "application/vnd.dynageo") (gex . "application/vnd.geometry-explorer") (ggb . "application/vnd.geogebra.file") (ggs . "application/vnd.geogebra.slides") (ggt . "application/vnd.geogebra.tool") (ghf . "application/vnd.groove-help") (gif . "image/gif") (gim . "application/vnd.groove-identity-message") (gml . "application/gml+xml") (gmx . "application/vnd.gmx") (gnumeric . "application/x-gnumeric") (gph . "application/vnd.flographit") (gpx . "application/gpx+xml") (gqf . "application/vnd.grafeq") (gqs . "application/vnd.grafeq") (gram . "application/srgs") (gramps . "application/x-gramps-xml") (gre . "application/vnd.geometry-explorer") (grv . "application/vnd.groove-injector") (grxml . "application/srgs+xml") (gsf . "application/x-font-ghostscript") (gtar . "application/x-gtar") (gtm . "application/vnd.groove-tool-message") (gtw . "model/vnd.gtw") (gv . "text/vnd.graphviz") (gxf . "application/gxf") (gxt . "application/vnd.geonext") (h . "text/x-c") (h261 . "video/h261") (h263 . "video/h263") (h264 . "video/h264") (hal . "application/vnd.hal+xml") (hbci . "application/vnd.hbci") (hdf . "application/x-hdf") (hh . "text/x-c") (hlp . "application/winhlp") (hpgl . "application/vnd.hp-hpgl") (hpid . "application/vnd.hp-hpid") (hps . "application/vnd.hp-hps") (hqx . "application/mac-binhex40") (htke . "application/vnd.kenameaapp") (htm . "text/html") (html . "text/html") (hvd . "application/vnd.yamaha.hv-dic") (hvp . "application/vnd.yamaha.hv-voice") (hvs . "application/vnd.yamaha.hv-script") (i2g . "application/vnd.intergeo") (icc . "application/vnd.iccprofile") (ice . "x-conference/x-cooltalk") (icm . "application/vnd.iccprofile") (ico . "image/x-icon") (ics . "text/calendar") (ief . "image/ief") (ifb . "text/calendar") (ifm . "application/vnd.shana.informed.formdata") (iges . "model/iges") (igl . "application/vnd.igloader") (igm . "application/vnd.insors.igm") (igs . "model/iges") (igx . "application/vnd.micrografx.igx") (iif . "application/vnd.shana.informed.interchange") (imp . "application/vnd.accpac.simply.imp") (ims . "application/vnd.ms-ims") (in . "text/plain") (ink . "application/inkml+xml") (inkml . "application/inkml+xml") (install . "application/x-install-instructions") (iota . "application/vnd.astraea-software.iota") (ipfix . "application/ipfix") (ipk . "application/vnd.shana.informed.package") (irm . "application/vnd.ibm.rights-management") (irp . "application/vnd.irepository.package+xml") (iso . "application/x-iso9660-image") (itp . "application/vnd.shana.informed.formtemplate") (ivp . "application/vnd.immervision-ivp") (ivu . "application/vnd.immervision-ivu") (jad . "text/vnd.sun.j2me.app-descriptor") (jam . "application/vnd.jam") (jar . "application/java-archive") (java . "text/x-java-source") (jisp . "application/vnd.jisp") (jlt . "application/vnd.hp-jlyt") (jnlp . "application/x-java-jnlp-file") (joda . "application/vnd.joost.joda-archive") (jpe . "image/jpeg") (jpeg . "image/jpeg") (jpg . "image/jpeg") (jpgm . "video/jpm") (jpgv . "video/jpeg") (jpm . "video/jpm") (js . "text/javascript") (json . "application/json") (jsonml . "application/jsonml+json") (kar . "audio/midi") (karbon . "application/vnd.kde.karbon") (kfo . "application/vnd.kde.kformula") (kia . "application/vnd.kidspiration") (kml . "application/vnd.google-earth.kml+xml") (kmz . "application/vnd.google-earth.kmz") (kne . "application/vnd.kinar") (knp . "application/vnd.kinar") (kon . "application/vnd.kde.kontour") (kpr . "application/vnd.kde.kpresenter") (kpt . "application/vnd.kde.kpresenter") (kpxx . "application/vnd.ds-keypoint") (ksp . "application/vnd.kde.kspread") (ktr . "application/vnd.kahootz") (ktx . "image/ktx") (ktz . "application/vnd.kahootz") (kwd . "application/vnd.kde.kword") (kwt . "application/vnd.kde.kword") (lasxml . "application/vnd.las.las+xml") (latex . "application/x-latex") (lbd . "application/vnd.llamagraphics.life-balance.desktop") (lbe . "application/vnd.llamagraphics.life-balance.exchange+xml") (les . "application/vnd.hhe.lesson-player") (lha . "application/x-lzh-compressed") (link66 . "application/vnd.route66.link66+xml") (list . "text/plain") (list3820 . "application/vnd.ibm.modcap") (listafp . "application/vnd.ibm.modcap") (lnk . "application/x-ms-shortcut") (log . "text/plain") (lostxml . "application/lost+xml") (lrf . "application/octet-stream") (lrm . "application/vnd.ms-lrm") (ltf . "application/vnd.frogans.ltf") (lvp . "audio/vnd.lucent.voice") (lwp . "application/vnd.lotus-wordpro") (lzh . "application/x-lzh-compressed") (m13 . "application/x-msmediaview") (m14 . "application/x-msmediaview") (m1v . "video/mpeg") (m21 . "application/mp21") (m2a . "audio/mpeg") (m2t . "video/mp2t") (m2ts . "video/mp2t") (m2v . "video/mpeg") (m3a . "audio/mpeg") (m3u . "audio/x-mpegurl") (m3u8 . "application/vnd.apple.mpegurl") (m4a . "audio/mp4") (m4u . "video/vnd.mpegurl") (m4v . "video/x-m4v") (ma . "application/mathematica") (mads . "application/mads+xml") (mag . "application/vnd.ecowin.chart") (maker . "application/vnd.framemaker") (man . "text/troff") (mar . "application/octet-stream") (mathml . "application/mathml+xml") (mb . "application/mathematica") (mbk . "application/vnd.mobius.mbk") (mbox . "application/mbox") (mc1 . "application/vnd.medcalcdata") (mcd . "application/vnd.mcd") (mcurl . "text/vnd.curl.mcurl") (mdb . "application/x-msaccess") (mdi . "image/vnd.ms-modi") (me . "text/troff") (mesh . "model/mesh") (meta4 . "application/metalink4+xml") (metalink . "application/metalink+xml") (mets . "application/mets+xml") (mfm . "application/vnd.mfmp") (mft . "application/rpki-manifest") (mgp . "application/vnd.osgeo.mapguide.package") (mgz . "application/vnd.proteus.magazine") (mid . "audio/midi") (midi . "audio/midi") (mie . "application/x-mie") (mif . "application/vnd.mif") (mime . "message/rfc822") (mj2 . "video/mj2") (mjp2 . "video/mj2") (mjs . "text/javascript") (mk3d . "video/x-matroska") (mka . "audio/x-matroska") (mks . "video/x-matroska") (mkv . "video/x-matroska") (mlp . "application/vnd.dolby.mlp") (mmd . "application/vnd.chipnuts.karaoke-mmd") (mmf . "application/vnd.smaf") (mmr . "image/vnd.fujixerox.edmics-mmr") (mng . "video/x-mng") (mny . "application/x-msmoney") (mobi . "application/x-mobipocket-ebook") (mods . "application/mods+xml") (mov . "video/quicktime") (movie . "video/x-sgi-movie") (mp2 . "audio/mpeg") (mp21 . "application/mp21") (mp2a . "audio/mpeg") (mp3 . "audio/mpeg") (mp4 . "video/mp4") (mp4a . "audio/mp4") (mp4s . "application/mp4") (mp4v . "video/mp4") (mpc . "application/vnd.mophun.certificate") (mpe . "video/mpeg") (mpeg . "video/mpeg") (mpg . "video/mpeg") (mpg4 . "video/mp4") (mpga . "audio/mpeg") (mpkg . "application/vnd.apple.installer+xml") (mpm . "application/vnd.blueice.multipass") (mpn . "application/vnd.mophun.application") (mpp . "application/vnd.ms-project") (mpt . "application/vnd.ms-project") (mpy . "application/vnd.ibm.minipay") (mqy . "application/vnd.mobius.mqy") (mrc . "application/marc") (mrcx . "application/marcxml+xml") (ms . "text/troff") (mscml . "application/mediaservercontrol+xml") (mseed . "application/vnd.fdsn.mseed") (mseq . "application/vnd.mseq") (msf . "application/vnd.epson.msf") (msh . "model/mesh") (msi . "application/x-msdownload") (msl . "application/vnd.mobius.msl") (msty . "application/vnd.muvee.style") (mts . "video/mp2t") (mus . "application/vnd.musician") (musicxml . "application/vnd.recordare.musicxml+xml") (mvb . "application/x-msmediaview") (mwf . "application/vnd.mfer") (mxf . "application/mxf") (mxl . "application/vnd.recordare.musicxml") (mxml . "application/xv+xml") (mxs . "application/vnd.triscape.mxs") (mxu . "video/vnd.mpegurl") (n-gage . "application/vnd.nokia.n-gage.symbian.install") (n3 . "text/n3") (nb . "application/mathematica") (nbp . "application/vnd.wolfram.player") (nc . "application/x-netcdf") (ncx . "application/x-dtbncx+xml") (nfo . "text/x-nfo") (ngdat . "application/vnd.nokia.n-gage.data") (nitf . "application/vnd.nitf") (nlu . "application/vnd.neurolanguage.nlu") (nml . "application/vnd.enliven") (nnd . "application/vnd.noblenet-directory") (nns . "application/vnd.noblenet-sealer") (nnw . "application/vnd.noblenet-web") (npx . "image/vnd.net-fpx") (nsc . "application/x-conference") (nsf . "application/vnd.lotus-notes") (ntf . "application/vnd.nitf") (nzb . "application/x-nzb") (oa2 . "application/vnd.fujitsu.oasys2") (oa3 . "application/vnd.fujitsu.oasys3") (oas . "application/vnd.fujitsu.oasys") (obd . "application/x-msbinder") (obj . "application/x-tgif") (oda . "application/oda") (odb . "application/vnd.oasis.opendocument.database") (odc . "application/vnd.oasis.opendocument.chart") (odf . "application/vnd.oasis.opendocument.formula") (odft . "application/vnd.oasis.opendocument.formula-template") (odg . "application/vnd.oasis.opendocument.graphics") (odi . "application/vnd.oasis.opendocument.image") (odm . "application/vnd.oasis.opendocument.text-master") (odp . "application/vnd.oasis.opendocument.presentation") (ods . "application/vnd.oasis.opendocument.spreadsheet") (odt . "application/vnd.oasis.opendocument.text") (oga . "audio/ogg") (ogg . "audio/ogg") (ogv . "video/ogg") (ogx . "application/ogg") (omdoc . "application/omdoc+xml") (onepkg . "application/onenote") (onetmp . "application/onenote") (onetoc . "application/onenote") (onetoc2 . "application/onenote") (opf . "application/oebps-package+xml") (opml . "text/x-opml") (oprc . "application/vnd.palm") (opus . "audio/ogg") (org . "application/vnd.lotus-organizer") (osf . "application/vnd.yamaha.openscoreformat") (osfpvg . "application/vnd.yamaha.openscoreformat.osfpvg+xml") (otc . "application/vnd.oasis.opendocument.chart-template") (otf . "font/otf") (otg . "application/vnd.oasis.opendocument.graphics-template") (oth . "application/vnd.oasis.opendocument.text-web") (oti . "application/vnd.oasis.opendocument.image-template") (otp . "application/vnd.oasis.opendocument.presentation-template") (ots . "application/vnd.oasis.opendocument.spreadsheet-template") (ott . "application/vnd.oasis.opendocument.text-template") (oxps . "application/oxps") (oxt . "application/vnd.openofficeorg.extension") (p . "text/x-pascal") (p10 . "application/pkcs10") (p12 . "application/x-pkcs12") (p7b . "application/x-pkcs7-certificates") (p7c . "application/pkcs7-mime") (p7m . "application/pkcs7-mime") (p7r . "application/x-pkcs7-certreqresp") (p7s . "application/pkcs7-signature") (p8 . "application/pkcs8") (pas . "text/x-pascal") (paw . "application/vnd.pawaafile") (pbd . "application/vnd.powerbuilder6") (pbm . "image/x-portable-bitmap") (pcap . "application/vnd.tcpdump.pcap") (pcf . "application/x-font-pcf") (pcl . "application/vnd.hp-pcl") (pclxl . "application/vnd.hp-pclxl") (pct . "image/x-pict") (pcurl . "application/vnd.curl.pcurl") (pcx . "image/x-pcx") (pdb . "application/vnd.palm") (pdf . "application/pdf") (pfa . "application/x-font-type1") (pfb . "application/x-font-type1") (pfm . "application/x-font-type1") (pfr . "application/font-tdpfr") (pfx . "application/x-pkcs12") (pgm . "image/x-portable-graymap") (pgn . "application/x-chess-pgn") (pgp . "application/pgp-encrypted") (pic . "image/x-pict") (pkg . "application/octet-stream") (pki . "application/pkixcmp") (pkipath . "application/pkix-pkipath") (plb . "application/vnd.3gpp.pic-bw-large") (plc . "application/vnd.mobius.plc") (plf . "application/vnd.pocketlearn") (pls . "application/pls+xml") (pml . "application/vnd.ctc-posml") (png . "image/png") (pnm . "image/x-portable-anymap") (portpkg . "application/vnd.macports.portpkg") (pot . "application/vnd.ms-powerpoint") (potm . "application/vnd.ms-powerpoint.template.macroenabled.12") (potx . "application/vnd.openxmlformats-officedocument.presentationml.template") (ppam . "application/vnd.ms-powerpoint.addin.macroenabled.12") (ppd . "application/vnd.cups-ppd") (ppm . "image/x-portable-pixmap") (pps . "application/vnd.ms-powerpoint") (ppsm . "application/vnd.ms-powerpoint.slideshow.macroenabled.12") (ppsx . "application/vnd.openxmlformats-officedocument.presentationml.slideshow") (ppt . "application/vnd.ms-powerpoint") (pptm . "application/vnd.ms-powerpoint.presentation.macroenabled.12") (pptx . "application/vnd.openxmlformats-officedocument.presentationml.presentation") (pqa . "application/vnd.palm") (prc . "application/x-mobipocket-ebook") (pre . "application/vnd.lotus-freelance") (prf . "application/pics-rules") (ps . "application/postscript") (psb . "application/vnd.3gpp.pic-bw-small") (psd . "image/vnd.adobe.photoshop") (psf . "application/x-font-linux-psf") (pskcxml . "application/pskc+xml") (ptid . "application/vnd.pvi.ptid1") (pub . "application/x-mspublisher") (pvb . "application/vnd.3gpp.pic-bw-var") (pwn . "application/vnd.3m.post-it-notes") (pya . "audio/vnd.ms-playready.media.pya") (pyv . "video/vnd.ms-playready.media.pyv") (qam . "application/vnd.epson.quickanime") (qbo . "application/vnd.intu.qbo") (qfx . "application/vnd.intu.qfx") (qps . "application/vnd.publishare-delta-tree") (qt . "video/quicktime") (qwd . "application/vnd.quark.quarkxpress") (qwt . "application/vnd.quark.quarkxpress") (qxb . "application/vnd.quark.quarkxpress") (qxd . "application/vnd.quark.quarkxpress") (qxl . "application/vnd.quark.quarkxpress") (qxt . "application/vnd.quark.quarkxpress") (ra . "audio/x-pn-realaudio") (ram . "audio/x-pn-realaudio") (rar . "application/x-rar-compressed") (ras . "image/x-cmu-raster") (rcprofile . "application/vnd.ipunplugged.rcprofile") (rdf . "application/rdf+xml") (rdz . "application/vnd.data-vision.rdz") (rep . "application/vnd.businessobjects") (res . "application/x-dtbresource+xml") (rgb . "image/x-rgb") (rif . "application/reginfo+xml") (rip . "audio/vnd.rip") (ris . "application/x-research-info-systems") (rl . "application/resource-lists+xml") (rlc . "image/vnd.fujixerox.edmics-rlc") (rld . "application/resource-lists-diff+xml") (rm . "application/vnd.rn-realmedia") (rmi . "audio/midi") (rmp . "audio/x-pn-realaudio-plugin") (rms . "application/vnd.jcp.javame.midlet-rms") (rmvb . "application/vnd.rn-realmedia-vbr") (rnc . "application/relax-ng-compact-syntax") (roa . "application/rpki-roa") (roff . "text/troff") (rp9 . "application/vnd.cloanto.rp9") (rpss . "application/vnd.nokia.radio-presets") (rpst . "application/vnd.nokia.radio-preset") (rq . "application/sparql-query") (rs . "application/rls-services+xml") (rsd . "application/rsd+xml") (rss . "application/rss+xml") (rtf . "application/rtf") (rtx . "text/richtext") (s . "text/x-asm") (s3m . "audio/s3m") (saf . "application/vnd.yamaha.smaf-audio") (sbml . "application/sbml+xml") (sc . "application/vnd.ibm.secure-container") (scd . "application/x-msschedule") (scm . "application/vnd.lotus-screencam") (scq . "application/scvp-cv-request") (scs . "application/scvp-cv-response") (scurl . "text/vnd.curl.scurl") (sda . "application/vnd.stardivision.draw") (sdc . "application/vnd.stardivision.calc") (sdd . "application/vnd.stardivision.impress") (sdkd . "application/vnd.solent.sdkm+xml") (sdkm . "application/vnd.solent.sdkm+xml") (sdp . "application/sdp") (sdw . "application/vnd.stardivision.writer") (see . "application/vnd.seemail") (seed . "application/vnd.fdsn.seed") (sema . "application/vnd.sema") (semd . "application/vnd.semd") (semf . "application/vnd.semf") (ser . "application/java-serialized-object") (setpay . "application/set-payment-initiation") (setreg . "application/set-registration-initiation") (sfd-hdstx . "application/vnd.hydrostatix.sof-data") (sfs . "application/vnd.spotfire.sfs") (sfv . "text/x-sfv") (sgi . "image/sgi") (sgl . "application/vnd.stardivision.writer-global") (sgm . "text/sgml") (sgml . "text/sgml") (sh . "application/x-sh") (shar . "application/x-shar") (shf . "application/shf+xml") (sid . "image/x-mrsid-image") (sig . "application/pgp-signature") (sil . "audio/silk") (silo . "model/mesh") (sis . "application/vnd.symbian.install") (sisx . "application/vnd.symbian.install") (sit . "application/x-stuffit") (sitx . "application/x-stuffitx") (skd . "application/vnd.koan") (skm . "application/vnd.koan") (skp . "application/vnd.koan") (skt . "application/vnd.koan") (sldm . "application/vnd.ms-powerpoint.slide.macroenabled.12") (sldx . "application/vnd.openxmlformats-officedocument.presentationml.slide") (slt . "application/vnd.epson.salt") (sm . "application/vnd.stepmania.stepchart") (smf . "application/vnd.stardivision.math") (smi . "application/smil+xml") (smil . "application/smil+xml") (smv . "video/x-smv") (smzip . "application/vnd.stepmania.package") (snd . "audio/basic") (snf . "application/x-font-snf") (so . "application/octet-stream") (spc . "application/x-pkcs7-certificates") (spf . "application/vnd.yamaha.smaf-phrase") (spl . "application/x-futuresplash") (spot . "text/vnd.in3d.spot") (spp . "application/scvp-vp-response") (spq . "application/scvp-vp-request") (spx . "audio/ogg") (sql . "application/x-sql") (src . "application/x-wais-source") (srt . "application/x-subrip") (sru . "application/sru+xml") (srx . "application/sparql-results+xml") (ssdl . "application/ssdl+xml") (sse . "application/vnd.kodak-descriptor") (ssf . "application/vnd.epson.ssf") (ssml . "application/ssml+xml") (st . "application/vnd.sailingtracker.track") (stc . "application/vnd.sun.xml.calc.template") (std . "application/vnd.sun.xml.draw.template") (stf . "application/vnd.wt.stf") (sti . "application/vnd.sun.xml.impress.template") (stk . "application/hyperstudio") (stl . "application/vnd.ms-pki.stl") (str . "application/vnd.pg.format") (stw . "application/vnd.sun.xml.writer.template") (sub . "text/vnd.dvb.subtitle") (sus . "application/vnd.sus-calendar") (susp . "application/vnd.sus-calendar") (sv4cpio . "application/x-sv4cpio") (sv4crc . "application/x-sv4crc") (svc . "application/vnd.dvb.service") (svd . "application/vnd.svd") (svg . "image/svg+xml") (svgz . "image/svg+xml") (swa . "application/x-director") (swf . "application/x-shockwave-flash") (swi . "application/vnd.aristanetworks.swi") (sxc . "application/vnd.sun.xml.calc") (sxd . "application/vnd.sun.xml.draw") (sxg . "application/vnd.sun.xml.writer.global") (sxi . "application/vnd.sun.xml.impress") (sxm . "application/vnd.sun.xml.math") (sxw . "application/vnd.sun.xml.writer") (t . "text/troff") (t3 . "application/x-t3vm-image") (taglet . "application/vnd.mynfc") (tao . "application/vnd.tao.intent-module-archive") (tar . "application/x-tar") (tcap . "application/vnd.3gpp2.tcap") (tcl . "application/x-tcl") (teacher . "application/vnd.smart.teacher") (tei . "application/tei+xml") (teicorpus . "application/tei+xml") (tex . "application/x-tex") (texi . "application/x-texinfo") (texinfo . "application/x-texinfo") (text . "text/plain") (tfi . "application/thraud+xml") (tfm . "application/x-tex-tfm") (tga . "image/x-tga") (thmx . "application/vnd.ms-officetheme") (tif . "image/tiff") (tiff . "image/tiff") (tmo . "application/vnd.tmobile-livetv") (torrent . "application/x-bittorrent") (tpl . "application/vnd.groove-tool-template") (tpt . "application/vnd.trid.tpt") (tr . "text/troff") (tra . "application/vnd.trueapp") (trm . "application/x-msterminal") (ts . "video/mp2t") (tsd . "application/timestamped-data") (tsv . "text/tab-separated-values") (ttc . "font/collection") (ttf . "font/ttf") (ttl . "text/turtle") (twd . "application/vnd.simtech-mindmapper") (twds . "application/vnd.simtech-mindmapper") (txd . "application/vnd.genomatix.tuxedo") (txf . "application/vnd.mobius.txf") (txt . "text/plain") (u32 . "application/x-authorware-bin") (udeb . "application/x-debian-package") (ufd . "application/vnd.ufdl") (ufdl . "application/vnd.ufdl") (ulx . "application/x-glulx") (umj . "application/vnd.umajin") (unityweb . "application/vnd.unity") (uoml . "application/vnd.uoml+xml") (uri . "text/uri-list") (uris . "text/uri-list") (urls . "text/uri-list") (ustar . "application/x-ustar") (utz . "application/vnd.uiq.theme") (uu . "text/x-uuencode") (uva . "audio/vnd.dece.audio") (uvd . "application/vnd.dece.data") (uvf . "application/vnd.dece.data") (uvg . "image/vnd.dece.graphic") (uvh . "video/vnd.dece.hd") (uvi . "image/vnd.dece.graphic") (uvm . "video/vnd.dece.mobile") (uvp . "video/vnd.dece.pd") (uvs . "video/vnd.dece.sd") (uvt . "application/vnd.dece.ttml+xml") (uvu . "video/vnd.uvvu.mp4") (uvv . "video/vnd.dece.video") (uvva . "audio/vnd.dece.audio") (uvvd . "application/vnd.dece.data") (uvvf . "application/vnd.dece.data") (uvvg . "image/vnd.dece.graphic") (uvvh . "video/vnd.dece.hd") (uvvi . "image/vnd.dece.graphic") (uvvm . "video/vnd.dece.mobile") (uvvp . "video/vnd.dece.pd") (uvvs . "video/vnd.dece.sd") (uvvt . "application/vnd.dece.ttml+xml") (uvvu . "video/vnd.uvvu.mp4") (uvvv . "video/vnd.dece.video") (uvvx . "application/vnd.dece.unspecified") (uvvz . "application/vnd.dece.zip") (uvx . "application/vnd.dece.unspecified") (uvz . "application/vnd.dece.zip") (vcard . "text/vcard") (vcd . "application/x-cdlink") (vcf . "text/x-vcard") (vcg . "application/vnd.groove-vcard") (vcs . "text/x-vcalendar") (vcx . "application/vnd.vcx") (vis . "application/vnd.visionary") (viv . "video/vnd.vivo") (vob . "video/x-ms-vob") (vor . "application/vnd.stardivision.writer") (vox . "application/x-authorware-bin") (vrml . "model/vrml") (vsd . "application/vnd.visio") (vsf . "application/vnd.vsf") (vss . "application/vnd.visio") (vst . "application/vnd.visio") (vsw . "application/vnd.visio") (vtu . "model/vnd.vtu") (vxml . "application/voicexml+xml") (w3d . "application/x-director") (wad . "application/x-doom") (wasm . "application/wasm") (wav . "audio/x-wav") (wax . "audio/x-ms-wax") (wbmp . "image/vnd.wap.wbmp") (wbs . "application/vnd.criticaltools.wbs+xml") (wbxml . "application/vnd.wap.wbxml") (wcm . "application/vnd.ms-works") (wdb . "application/vnd.ms-works") (wdp . "image/vnd.ms-photo") (weba . "audio/webm") (webm . "video/webm") (webp . "image/webp") (wg . "application/vnd.pmi.widget") (wgt . "application/widget") (wks . "application/vnd.ms-works") (wm . "video/x-ms-wm") (wma . "audio/x-ms-wma") (wmd . "application/x-ms-wmd") (wmf . "application/x-msmetafile") (wml . "text/vnd.wap.wml") (wmlc . "application/vnd.wap.wmlc") (wmls . "text/vnd.wap.wmlscript") (wmlsc . "application/vnd.wap.wmlscriptc") (wmv . "video/x-ms-wmv") (wmx . "video/x-ms-wmx") (wmz . "application/x-msmetafile") (woff . "font/woff") (woff2 . "font/woff2") (wpd . "application/vnd.wordperfect") (wpl . "application/vnd.ms-wpl") (wps . "application/vnd.ms-works") (wqd . "application/vnd.wqd") (wri . "application/x-mswrite") (wrl . "model/vrml") (wsdl . "application/wsdl+xml") (wspolicy . "application/wspolicy+xml") (wtb . "application/vnd.webturbo") (wvx . "video/x-ms-wvx") (x32 . "application/x-authorware-bin") (x3d . "model/x3d+xml") (x3db . "model/x3d+binary") (x3dbz . "model/x3d+binary") (x3dv . "model/x3d+vrml") (x3dvz . "model/x3d+vrml") (x3dz . "model/x3d+xml") (xaml . "application/xaml+xml") (xap . "application/x-silverlight-app") (xar . "application/vnd.xara") (xbap . "application/x-ms-xbap") (xbd . "application/vnd.fujixerox.docuworks.binder") (xbm . "image/x-xbitmap") (xdf . "application/xcap-diff+xml") (xdm . "application/vnd.syncml.dm+xml") (xdp . "application/vnd.adobe.xdp+xml") (xdssc . "application/dssc+xml") (xdw . "application/vnd.fujixerox.docuworks") (xenc . "application/xenc+xml") (xer . "application/patch-ops-error+xml") (xfdf . "application/vnd.adobe.xfdf") (xfdl . "application/vnd.xfdl") (xht . "application/xhtml+xml") (xhtml . "application/xhtml+xml") (xhvml . "application/xv+xml") (xif . "image/vnd.xiff") (xla . "application/vnd.ms-excel") (xlam . "application/vnd.ms-excel.addin.macroenabled.12") (xlc . "application/vnd.ms-excel") (xlf . "application/x-xliff+xml") (xlm . "application/vnd.ms-excel") (xls . "application/vnd.ms-excel") (xlsb . "application/vnd.ms-excel.sheet.binary.macroenabled.12") (xlsm . "application/vnd.ms-excel.sheet.macroenabled.12") (xlsx . "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") (xlt . "application/vnd.ms-excel") (xltm . "application/vnd.ms-excel.template.macroenabled.12") (xltx . "application/vnd.openxmlformats-officedocument.spreadsheetml.template") (xlw . "application/vnd.ms-excel") (xm . "audio/xm") (xml . "application/xml") (xo . "application/vnd.olpc-sugar") (xop . "application/xop+xml") (xpi . "application/x-xpinstall") (xpl . "application/xproc+xml") (xpm . "image/x-xpixmap") (xpr . "application/vnd.is-xpr") (xps . "application/vnd.ms-xpsdocument") (xpw . "application/vnd.intercon.formnet") (xpx . "application/vnd.intercon.formnet") (xsl . "application/xml") (xslt . "application/xslt+xml") (xsm . "application/vnd.syncml+xml") (xspf . "application/xspf+xml") (xul . "application/vnd.mozilla.xul+xml") (xvm . "application/xv+xml") (xvml . "application/xv+xml") (xwd . "image/x-xwindowdump") (xyz . "chemical/x-xyz") (xz . "application/x-xz") (yang . "application/yang") (yin . "application/yin+xml") (z1 . "application/x-zmachine") (z2 . "application/x-zmachine") (z3 . "application/x-zmachine") (z4 . "application/x-zmachine") (z5 . "application/x-zmachine") (z6 . "application/x-zmachine") (z7 . "application/x-zmachine") (z8 . "application/x-zmachine") (zaz . "application/vnd.zzazz.deck+xml") (zip . "application/zip") (zir . "application/vnd.zul") (zirz . "application/vnd.zul") (zmm . "application/vnd.handheld-entertainment+xml")) -------------------------------------------------------------------------------- /splitflap-lib/private/validation.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | (require "dust.rkt" 4 | "xml-generic.rkt" 5 | gregor 6 | net/url-string 7 | racket/contract 8 | racket/match 9 | racket/path 10 | racket/promise 11 | racket/runtime-path 12 | racket/string 13 | xml) 14 | 15 | (provide dns-domain? 16 | email-address? 17 | validate-email-address 18 | tag-entity-date? 19 | tag-specific-string? 20 | tag-uri? 21 | mint-tag-uri 22 | append-specific 23 | tag-uri->string 24 | tag=? 25 | normalize-tag-specific 26 | infer-moment 27 | moment->string 28 | (rename-out [make-person person]) 29 | person? 30 | person->xexpr 31 | rss-dialect? 32 | (struct-out enclosure) 33 | file->enclosure 34 | mime-types-by-ext 35 | path/string->mime-type 36 | valid-url-string? 37 | url-domain 38 | url-join 39 | iso-639-language-code? 40 | language-codes 41 | system-language) 42 | 43 | ;; ~~ DNS Domain validation (RFC 1035) ~~~~~~~~~~~ 44 | ;; 45 | ;; ::= | " " 46 | ;; ::= 101 | XML 102 | ) 103 | (check-equal? (indented-xml-string test-xpr) expect-str) -------------------------------------------------------------------------------- /splitflap/info.rkt: -------------------------------------------------------------------------------- 1 | #lang info 2 | 3 | (define collection 'multi) 4 | 5 | (define deps '("splitflap-doc" 6 | "splitflap-lib")) 7 | (define implies '("splitflap-doc" 8 | "splitflap-lib")) 9 | 10 | (define pkg-desc "Validated Atom and RSS feed generation") 11 | (define license 'BlueOak-1.0.0) 12 | --------------------------------------------------------------------------------