bureaucromancy 0.1.0.1 → 0.1.0.2
raw patch · 28 files changed
+1017/−101 lines, 28 filesdep +aesondep ~basedep ~html-conduitdep ~http-typesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: aeson
Dependency ranges changed: base, html-conduit, http-types, text, wai, warp
API changes (from Hackage documentation)
+ Text.HTML.Form: [lang] :: Form -> String
+ Text.HTML.Form: parseDocument' :: Text -> Document -> Text -> Maybe Form
+ Text.HTML.Form: parseElement' :: Text -> Cursor -> Maybe Form
+ Text.HTML.Form.WebApp: [lang] :: Form -> String
- Text.HTML.Form: Form :: URI -> Text -> Text -> Bool -> Text -> [Text] -> Bool -> Text -> Text -> [Input] -> Form
+ Text.HTML.Form: Form :: URI -> Text -> Text -> Bool -> Text -> [Text] -> Bool -> Text -> Text -> [Input] -> String -> Form
- Text.HTML.Form.Colours: tailwindColours :: [(String, [(Int, Colour)])]
+ Text.HTML.Form.Colours: tailwindColours :: String -> [(String, [(Int, Colour)])]
- Text.HTML.Form.Validate: inputErrorMessage :: Input -> String
+ Text.HTML.Form.Validate: inputErrorMessage :: String -> Input -> String
- Text.HTML.Form.Validate: inputErrorMessage' :: Input -> [Char]
+ Text.HTML.Form.Validate: inputErrorMessage' :: String -> Input -> [Char]
- Text.HTML.Form.WebApp: Form :: URI -> Text -> Text -> Bool -> Text -> [Text] -> Bool -> Text -> Text -> [Input] -> Form
+ Text.HTML.Form.WebApp: Form :: URI -> Text -> Text -> Bool -> Text -> [Text] -> Bool -> Text -> Text -> [Input] -> String -> Form
Files
- app/Main.hs +1/−0
- bureaucromancy.cabal +8/−7
- i18n/en.txt +65/−0
- src/Text/HTML/Form.hs +16/−7
- src/Text/HTML/Form/Colours.hs +27/−25
- src/Text/HTML/Form/I18n.hs +54/−0
- src/Text/HTML/Form/Validate.hs +51/−54
- src/Text/HTML/Form/WebApp.hs +4/−3
- src/Text/HTML/Form/WebApp/Ginger.hs +8/−5
- tpl/base.html +71/−0
- tpl/cal/timezone.html +13/−0
- tpl/cal/year-numpad.html +28/−0
- tpl/checkbox.html +1/−0
- tpl/color.html +21/−0
- tpl/files.html +15/−0
- tpl/gregorian.html +91/−0
- tpl/image-button.html +15/−0
- tpl/keyboards/latin1.html +78/−0
- tpl/keyboards/latin1/latin1-accent-lower.html +70/−0
- tpl/keyboards/latin1/latin1-accent.html +71/−0
- tpl/keyboards/latin1/latin1-symbol.html +111/−0
- tpl/keyboards/latin1/latin1-upper.html +78/−0
- tpl/number.html +34/−0
- tpl/radio.html +1/−0
- tpl/reset.html +7/−0
- tpl/select.html +32/−0
- tpl/submit.html +14/−0
- tpl/tel.html +32/−0
app/Main.hs view
@@ -26,6 +26,7 @@ [] -> ("form.html", "0") doc <- HTML.readFile filename let form = ensureButtons $ fromJust $ parseDocument doc $ Txt.pack ident+ putStrLn "http://127.0.0.1:2018/" runEnv 2018 $ servePage form servePage :: Form -> Application
bureaucromancy.cabal view
@@ -20,7 +20,7 @@ -- PVP summary: +-+------- breaking API changes -- | | +----- non-breaking API additions -- | | | +--- code changes with no API change-version: 0.1.0.1+version: 0.1.0.2 -- A short (one-line) description of the package. synopsis: Parse webforms & render to interactive hypertext@@ -52,7 +52,7 @@ extra-doc-files: CHANGELOG.md -- Extra source files to be distributed with the package, such as examples, or a tutorial module.--- extra-source-files:+extra-source-files: tpl/**/*.html, i18n/*.txt common warnings ghc-options: -Wall@@ -68,7 +68,7 @@ Text.HTML.Form.Colours, Text.HTML.Form.Validate -- Modules included in this library but not exported.- -- other-modules:+ other-modules: Text.HTML.Form.I18n -- LANGUAGE extensions used by modules in this package. -- other-extensions:@@ -78,7 +78,7 @@ file-embed-lzma >=0.0.1 && <1, file-embed >=0.0.15 && < 0.1, mtl >2 && <3, bytestring >=0.11 && <1, text >=2 && <3, containers >=0.6 && <1, xml-conduit >= 1.9 && <2, network-uri >=2.6 && <3, regex-tdfa >=1.3 && <2,- filepath >=1.4 && <2, directory >=1.2 && <2,+ filepath >=1.4 && <2, directory >=1.2 && <2, aeson, hourglass >= 0.2.12 && < 0.3, tz >= 0.1 && < 0.2 -- Directories containing source files.@@ -102,8 +102,9 @@ -- Other library packages from which modules are imported. build-depends:- base >=4.16.4.0 && <5,- bureaucromancy, warp, wai, http-types, text, html-conduit+ base >=4.16.4.0 && <5, bureaucromancy,+ warp >= 3.3.31 && < 3.4, wai >= 3.2.3 && < 3.3, http-types >= 0.12.3 && < 0.13,+ text >= 2.0.1 && < 2.1, html-conduit >= 1.3.2 && < 1.4 -- Directories containing source files. hs-source-dirs: app@@ -135,5 +136,5 @@ -- Test dependencies. build-depends:- base ^>=4.16.4.0,+ base >=4.16.4.0 && <5, bureaucromancy
+ i18n/en.txt view
@@ -0,0 +1,65 @@+# Colour Pallet+Slate: Slate+Gray: Gray+Zinc: Zinc+Neutral: Neutral+Stone: Stone+Red: Red+Orange: Orange+Amber: Amber+Yellow: Yellow+Lime: Lime+Green: Green+Emerald: Emerald+Teal: Teal+Cyan: Cyan+Sky: Sky+Blue: Blue+Indigo: Indigo+Violet: Violet+Fuchsia: Fuchsia+Pink: Pink+Rose: Rose++# Error messages+err required: Required!+err format: Invalid format!+err min chars: Must be at least %0 characters!+err max chars: Must be at most %0 characters!+err min: Must be at least %0!+err max: Must be at most %0!+err increments: Must be in increments of %0 from %1!+err colour: Invalid colour value!+err email: Obviously invalid email address, needs an '@'!+err number: Invalid number!+err URL: Invalid web address!+err time: Invalid time format!+errored: Please correct errors listed in sidebar before submitting this form!++# Months+January: January+February: February+March: March+April: April+May: May+June: June+July: July+August: August+September: September+October: October+November: November+December: December++# Keyboard+SPACE: SPACE+DEL: DEL+CLEAR: CLEAR++# Other+Start!: Start!+Now: Now+# Combines to "Upload to" with "Upload" linked.+# Not great idea to split this phrase into 2 strings, but I don't want to link "to"...+Upload Link: Upload+Upload suffix: to+Restore defaults: Restore defaults
src/Text/HTML/Form.hs view
@@ -2,7 +2,8 @@ -- | Parse webforms out of webpages module Text.HTML.Form (Form(..), Input(..), OptionGroup(..), Option(..), FileSelector(..), defaultFileData, ImageData(..), defaultImageData,- TextArea(..), defaultTextArea, parseElement, parseDocument, ensureButtons) where+ TextArea(..), defaultTextArea, parseElement, parseElement',+ parseDocument, parseDocument', ensureButtons) where import Data.Text (Text) import qualified Data.Text as Txt@@ -39,7 +40,9 @@ -- | The purpose of this form, typically using an external vocabulary. rel :: Text, -- | What data should be sent to the endpoint.- inputs :: [Input]+ inputs :: [Input],+ -- | Which human language the form is written? To which additional messages should be localized?+ lang :: String } -- | Individual piece of data to send to a webservice.@@ -189,7 +192,9 @@ mAttr n = listToMaybe . laxAttribute n -- | Parse a form from the selected HTML element. parseElement :: Cursor -> Maybe Form-parseElement el | _:_ <- laxElement "form" el = Just Form {+parseElement = parseElement' "en"+parseElement' :: Text -> Cursor -> Maybe Form+parseElement' language el | _:_ <- laxElement "form" el = Just Form { action = attr'' "action" el (fromMaybe nullURI . parseURIReference) ".", enctype = attr "enctype" el "", method = attr "method" el "GET",@@ -199,7 +204,9 @@ autocomplete = hasAttr "autocomplete" el, formName = attr "name" el "", rel = attr "rel" el "",- inputs = mapMaybe parseInput $ queryInputs el+ inputs = mapMaybe parseInput $ queryInputs el,+ lang = Txt.unpack $ fromMaybe language $ listToMaybe $+ mapMaybe (mAttr "lang") $ orSelf ancestor el } | otherwise = Nothing @@ -431,10 +438,12 @@ -- | Parse a named or numerically-indexed form from an HTML document. parseDocument :: Document -> Text -> Maybe Form-parseDocument doc n+parseDocument = parseDocument' "en"+parseDocument' :: Text -> Document -> Text -> Maybe Form+parseDocument' language doc n | Just n' <- readMaybe $ Txt.unpack n, n' < length (forms doc') =- parseElement (forms doc' !! n')- | el:_ <- (forms >=> attributeIs "name" n) doc' = parseElement el+ parseElement' language (forms doc' !! n')+ | el:_ <- (forms >=> attributeIs "name" n) doc' = parseElement' language el | otherwise = Nothing where forms = orSelf descendant >=> laxElement "form"
src/Text/HTML/Form/Colours.hs view
@@ -2,6 +2,8 @@ -- for where we can't provide free-form colour selection. module Text.HTML.Form.Colours(tailwindColours, Colour) where +import Text.HTML.Form.I18n (i18n)+ -- Finally a good use for Tailwind! -- Otherwise it strikes me as little more than an illegible alternative to -- the `style` attribute!@@ -9,75 +11,75 @@ -- | Stores a CSS hexcode, with preceding "#" type Colour = String -- | The Tailwind colour pallet: https://v1.tailwindcss.com/docs/customizing-colors#default-color-palette-tailwindColours :: [(String, [(Int, Colour)])]-tailwindColours = [- "Slate"~>[50~>"#f8fafc", 100~>"#f1f5f9", 200~>"#e2e8f0", 300~>"#cbd5e1",+tailwindColours :: String -> [(String, [(Int, Colour)])]+tailwindColours lang = [+ i "Slate"~>[50~>"#f8fafc", 100~>"#f1f5f9", 200~>"#e2e8f0", 300~>"#cbd5e1", 400~>"#94a3b8", 500~>"#64748b", 600~>"#475569", 700~>"#334155", 800~>"#1e293b", 900~>"#0f172a", 950~>"#020617"],- "Gray"~>[50~>"#f9fafb", 100~>"#f3f4f6", 200~>"#e5e7eb", 300~>"#d1d5db",+ i "Gray"~>[50~>"#f9fafb", 100~>"#f3f4f6", 200~>"#e5e7eb", 300~>"#d1d5db", 400~>"#9ca3af", 500~>"#6b7280", 600~>"#4b5563", 700~>"#374151", 800~>"#1f2937", 900~>"#111827", 950~>"#030712"],- "Zinc"~>[50~>"#fafafa", 100~>"#f4f4f5", 200~>"#e4e4e7", 300~>"#d4d4d8",+ i "Zinc"~>[50~>"#fafafa", 100~>"#f4f4f5", 200~>"#e4e4e7", 300~>"#d4d4d8", 400~>"#a1a1aa", 500~>"#71717a", 600~>"#52525b", 700~>"#3f3f46", 800~>"#27272a", 900~>"#18181b", 950~>"#09090b"],- "Neutral"~>[50~>"#fafafa", 100~>"#f5f5f5", 200~>"#e5e5e5", 300~>"#d4d4d4",+ i "Neutral"~>[50~>"#fafafa", 100~>"#f5f5f5", 200~>"#e5e5e5", 300~>"#d4d4d4", 400~>"#a3a3a3", 500~>"#737373", 600~>"#404040", 700~>"#404040", 800~>"#262626", 900~>"#171717", 950~>"#0a0a0a"],- "Stone"~>[50~>"#fafaf9", 100~>"#f5f5f4", 200~>"#e7e5e4", 300~>"#d6d3d1",+ i "Stone"~>[50~>"#fafaf9", 100~>"#f5f5f4", 200~>"#e7e5e4", 300~>"#d6d3d1", 400~>"#a8a29e", 500~>"#78716c", 600~>"#57534e", 700~>"#44403c", 800~>"#292524", 900~>"#1c1917", 950~>"#0c0a09"],- "Red"~>[50~>"#fef2f2", 100~>"#fee2e2", 200~>"#fecaca", 300~>"#fca5a5",+ i "Red"~>[50~>"#fef2f2", 100~>"#fee2e2", 200~>"#fecaca", 300~>"#fca5a5", 400~>"#f87171", 500~>"#ef4444", 600~>"#dc2626", 700~>"#b91c1c", 800~>"#991b1b", 900~>"#7f1d1d", 950~>"#450a0a"],- "Orange"~>[50~>"#fff7ed", 100~>"#ffedd5", 200~>"#fed7aa", 300~>"#fdba74",+ i "Orange"~>[50~>"#fff7ed", 100~>"#ffedd5", 200~>"#fed7aa", 300~>"#fdba74", 400~>"#fb923c", 500~>"#f97316", 600~>"#ea580c", 700~>"#c2410c", 800~>"#9a3412", 900~>"#7c2d12", 950~>"#431407"],- "Amber"~>[50~>"#fffbeb", 100~>"#fef3c7", 200~>"#fde68a", 300~>"#fcd34d",+ i "Amber"~>[50~>"#fffbeb", 100~>"#fef3c7", 200~>"#fde68a", 300~>"#fcd34d", 400~>"#fbbf24", 500~>"#f59e0b", 600~>"#d97706", 700~>"#b45309", 800~>"#92400e", 900~>"#78350f", 950~>"#451a03"],- "Yellow"~>[50~>"#fefce8", 100~>"#fef9c3", 200~>"#fef08a", 300~>"#fde047",+ i "Yellow"~>[50~>"#fefce8", 100~>"#fef9c3", 200~>"#fef08a", 300~>"#fde047", 400~>"#facc15", 500~>"#eab308", 600~>"#ca8a04", 700~>"#a16207", 800~>"#854d0e", 900~>"#713f12", 950~>"#422006"],- "Lime"~>[50~>"#f7fee7", 100~>"#ecfccb", 200~>"#d9f99d", 300~>"#bef264",+ i "Lime"~>[50~>"#f7fee7", 100~>"#ecfccb", 200~>"#d9f99d", 300~>"#bef264", 400~>"#a3e635", 500~>"#84cc16", 600~>"#65a30d", 700~>"#4d7c0f", 800~>"#3f6212", 900~>"#365314", 950~>"#1a2e05"],- "Green"~>[50~>"#f0fdf4", 100~>"#dcfce7", 200~>"#bbf7d0", 300~>"#86efac",+ i "Green"~>[50~>"#f0fdf4", 100~>"#dcfce7", 200~>"#bbf7d0", 300~>"#86efac", 400~>"#4ade80", 500~>"#22c55e", 600~>"#16a34a", 700~>"#15803d", 800~>"#166534", 900~>"#14532d", 950~>"#052e16"],- "Emerald"~>[50~>"#ecfdf5", 100~>"#d1fae5", 200~>"#a7f3d0", 300~>"#6ee7b7",+ i "Emerald"~>[50~>"#ecfdf5", 100~>"#d1fae5", 200~>"#a7f3d0", 300~>"#6ee7b7", 400~>"#34d399", 500~>"#10b981", 600~>"#059669", 700~>"#047857", 800~>"#065f46", 900~>"#064e3b", 950~>"#022c22"],- "Teal"~>[50~>"#f0fdfa", 100~>"#ccfbf1", 200~>"#99f6e4", 300~>"#5eead4",+ i "Teal"~>[50~>"#f0fdfa", 100~>"#ccfbf1", 200~>"#99f6e4", 300~>"#5eead4", 400~>"#2dd4bf", 500~>"#14b8a6", 600~>"#0f766e", 700~>"#0f766e", 800~>"#115e59", 900~>"#134e4a", 950~>"#042f2e"],- "Cyan"~>[50~>"#ecfeff", 100~>"#cffafe", 200~>"#a5f3fc", 300~>"#67e8f9",+ i "Cyan"~>[50~>"#ecfeff", 100~>"#cffafe", 200~>"#a5f3fc", 300~>"#67e8f9", 400~>"#22d3ee", 500~>"#06b6d4", 600~>"#0891b2", 700~>"#0e7490", 800~>"#155e75", 900~>"#164e63", 950~>"#083344"],- "Sky"~>[50~>"#f0f9ff", 100~>"#e0f2fe", 200~>"#bae6fd", 300~>"#7dd3fc",+ i "Sky"~>[50~>"#f0f9ff", 100~>"#e0f2fe", 200~>"#bae6fd", 300~>"#7dd3fc", 400~>"#38bdf8", 500~>"#0ea5e9", 600~>"#0284c7", 700~>"#0369a1", 800~>"#075985", 900~>"#0c4a6e", 950~>"#082f49"],- "Blue"~>[50~>"#eff6ff", 100~>"#dbeafe", 200~>"#bfdbfe", 300~>"#93c5fd",+ i "Blue"~>[50~>"#eff6ff", 100~>"#dbeafe", 200~>"#bfdbfe", 300~>"#93c5fd", 400~>"#60a5fa", 500~>"#3b82f6", 600~>"#2563eb", 700~>"#1d4ed8", 800~>"#1e40af", 900~>"#1e3a8a", 950~>"#172554"],- "Indigo"~>[50~>"#eef2ff", 100~>"#e0e7ff", 200~>"#c7d2fe", 300~>"#a5b4fc",+ i "Indigo"~>[50~>"#eef2ff", 100~>"#e0e7ff", 200~>"#c7d2fe", 300~>"#a5b4fc", 400~>"#818cf8", 500~>"#6366f1", 600~>"#4f46e5", 700~>"#4338ca", 800~>"#3730a3", 900~>"#312e81", 950~>"#1e1b4b"],- "Violet"~>[50~>"#f5f3ff", 100~>"#ede9fe", 200~>"#ddd6fe", 300~>"#c4b5fd",+ i "Violet"~>[50~>"#f5f3ff", 100~>"#ede9fe", 200~>"#ddd6fe", 300~>"#c4b5fd", 400~>"#a78bfa", 500~>"#8b5cf6", 600~>"#7c3aed", 700~>"#6d28d9", 800~>"#5b21b6", 900~>"#4c1d95", 950~>"#2e1065"],- "Purple"~>[50~>"#faf5ff", 100~>"#f3e8ff", 200~>"#e9d5ff", 300~>"#d8b4fe",+ i "Purple"~>[50~>"#faf5ff", 100~>"#f3e8ff", 200~>"#e9d5ff", 300~>"#d8b4fe", 400~>"#c084fc", 500~>"#a855f7", 600~>"#9333ea", 700~>"#7e22ce", 800~>"#6b21a8", 900~>"#581c87", 950~>"#3b0764"],- "Fuchsia"~>[50~>"#fdf4ff", 100~>"#fae8ff", 200~>"#f5d0fe", 300~>"#f0abfc",+ i "Fuchsia"~>[50~>"#fdf4ff", 100~>"#fae8ff", 200~>"#f5d0fe", 300~>"#f0abfc", 400~>"#e879f9", 500~>"#d946ef", 600~>"#c026d3", 700~>"#a21caf", 800~>"#86198f", 900~>"#701a75", 950~>"#4a044e"],- "Pink"~>[50~>"#fdf2f8", 100~>"#fce7f3", 200~>"#fbcfe8", 300~>"#f9a8d4",+ i "Pink"~>[50~>"#fdf2f8", 100~>"#fce7f3", 200~>"#fbcfe8", 300~>"#f9a8d4", 400~>"#f472b6", 500~>"#ec4899", 600~>"#db2777", 700~>"#be185d", 800~>"#9d174d", 900~>"#831843", 950~>"#500724"],- "Rose"~>[50~>"#fff1f2", 100~>"#ffe4e6", 200~>"#fecdd3", 300~>"#fda4af",+ i "Rose"~>[50~>"#fff1f2", 100~>"#ffe4e6", 200~>"#fecdd3", 300~>"#fda4af", 400~>"#fb7185", 500~>"#f43f5e", 600~>"#e11d48", 700~>"#be123c", 800~>"#9f1239", 900~>"#881337", 950~>"#4c0519"]- ]+ ] where i = i18n lang (~>) :: a -> b -> (a, b) a ~> b = (a, b)
+ src/Text/HTML/Form/I18n.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE TemplateHaskell #-}+module Text.HTML.Form.I18n(strings, langs, i18n, i18n', i18n2, stringsJSON) where++import Data.FileEmbed (embedDir, makeRelativeToProject)+import Data.Text.Encoding (decodeUtf8Lenient)+import Data.Text (unpack, pack, strip, replace)+import Data.ByteString (ByteString)+import System.FilePath (dropExtension)++import qualified Data.Aeson.KeyMap as KM+import Data.Aeson.Key as K+import Data.Aeson (Value(String, Object))++import Data.List (isPrefixOf)+import Data.Maybe (fromMaybe)++files :: [(FilePath, ByteString)]+files = $(embedDir =<< makeRelativeToProject "i18n")+bs2str :: ByteString -> String+bs2str = unpack . decodeUtf8Lenient++strings :: String -> [(String, String)]+strings = fromMaybe [] .+ flip lookup [(dropExtension k, parseKVs $ bs2str v) | (k, v) <- files]+langs :: [String]+langs = map dropExtension $ map fst files++i18n :: String -> String -> String+i18n lang key = fromMaybe key $ lookup key $ strings lang++i18n' :: Show a => String -> String -> a -> String+i18n' lang key subs = replace' "%0" (show subs) $ i18n lang key+i18n2 :: (Show a, Show b) => String -> String -> a -> b -> String+i18n2 lang key subs1 subs2 = replace' "%1" (show subs2) $ i18n' lang key subs1++stringsJSON :: String -> Value+stringsJSON = Object . KM.fromList . map inner . strings+ where inner (k, v) = (K.fromString k, String $ pack v)++------+--- Support+------++parseKVs :: String -> [(String, String)]+parseKVs = map inner . filter (isPrefixOf "#") . filter null . map strip' . lines+ where inner line = let (k, v) = break (==':') line in (strip' k, strip' v)++-- | Removes any whitespace at the start or end of a string+strip' :: String -> String+strip' = unpack . strip . pack++-- | Substitutes one string for another+replace' :: String -> String -> String -> String+replace' needle alt = unpack . replace (pack needle) (pack alt) . pack
src/Text/HTML/Form/Validate.hs view
@@ -4,7 +4,8 @@ module Text.HTML.Form.Validate(isInputValid, isInputValid', isFormValid, isFormValid', inputErrorMessage, inputErrorMessage', normalizeInput, normalizeForm) where -import Text.HTML.Form+import Text.HTML.Form hiding (lang)+import Text.HTML.Form.I18n import qualified Data.Text as Txt import Text.Read (readMaybe) import Data.Hourglass@@ -22,82 +23,78 @@ -- | Is the given input valid? isInputValid :: Input -> Bool-isInputValid = null . inputErrorMessage+isInputValid = null . inputErrorMessage "en" -- | Is the given input once normalized valid? isInputValid' :: Input -> Bool-isInputValid' = null . inputErrorMessage'+isInputValid' = null . inputErrorMessage' "en" -- | Describe why a form input is invalid, or return the empty string.-inputErrorMessage :: Input -> String-inputErrorMessage Input { inputType = "hidden" } = "" -- Don't validate hiddens!-inputErrorMessage self@Input { required = True }- | inputType self == "checkbox", not $ checked self = "Required!"+inputErrorMessage :: String -> Input -> String+inputErrorMessage _ Input { inputType = "hidden" } = "" -- Don't validate hiddens!+inputErrorMessage lang self@Input { required = True }+ | inputType self == "checkbox", not $ checked self = i18n lang "err required" -- Not validating "radio", needs different API...- | value self == "" = "Required!"-inputErrorMessage Input { value = "" } = "" -- Skip further checks for empty!-inputErrorMessage self@Input { pattern = Just re }- | not $ re `matchTest` value self = "Invalid format!"-inputErrorMessage Input { lengthRange = (Just min', _), value = val }- | Txt.length val < min' = "Must be at least " ++ show min' ++ " characters!"-inputErrorMessage Input { lengthRange = (_, Just max'), value = val }- | Txt.length val > max' = "Must be at most " ++ show max' ++ " characters!"-inputErrorMessage Input { range = (Just min', _), value = val }+ | value self == "" = i18n lang "err required"+inputErrorMessage _ Input { value = "" } = "" -- Skip further checks for empty!+inputErrorMessage lang self@Input { pattern = Just re }+ | not $ re `matchTest` value self = i18n lang "err format"+inputErrorMessage lang Input { lengthRange = (Just min', _), value = val }+ | Txt.length val < min' = i18n' lang "err min chars" min'+inputErrorMessage lang Input { lengthRange = (_, Just max'), value = val }+ | Txt.length val > max' = i18n' lang "err max chars" max'+inputErrorMessage lang Input { range = (Just min', _), value = val } | Just x <- readMaybe' val :: Maybe Float, Just y <- readMaybe' min', x < y =- "Must be at least " ++ Txt.unpack min' ++ "!"-inputErrorMessage Input { range = (_, Just max'), value = val }+ i18n' lang "err min" min'+inputErrorMessage lang Input { range = (_, Just max'), value = val } | Just x <- readMaybe' val :: Maybe Float, Just y <- readMaybe' max', x > y =- "Must be at most " ++ Txt.unpack max' ++ "!"-inputErrorMessage Input { range = (Just min', _), step = Just step', value = val }+ i18n' lang "err max" max'+inputErrorMessage lang Input { range = (Just min', _), step = Just step', value = val } | Just x <- readMaybe' val :: Maybe Integer, Just y <- readMaybe' min', Just z <- readMaybe' step', z /= 0, rem (x - y) z == 0 =- ("Must be in increments of " ++ Txt.unpack step' ++ " from "- ++ Txt.unpack min' ++ "!")-inputErrorMessage Input { range = (Just min', _), value = val }+ i18n2 lang "err increments" step' min'+inputErrorMessage lang Input { range = (Just min', _), value = val } | Just x <- parseTime $ Txt.unpack val, Just y <- parseTime $ Txt.unpack min',- x < y = "Must be at least " ++ Txt.unpack min' ++ "!"-inputErrorMessage Input { range = (_, Just max'), value = val }+ x < y = i18n' lang "err min" min'+inputErrorMessage lang Input { range = (_, Just max'), value = val } | Just x <- parseTime $ Txt.unpack val, Just y <- parseTime $ Txt.unpack max',- x > y = "Must be at most " ++ Txt.unpack max' ++ "!"-inputErrorMessage Input {+ x > y = i18n' lang "err max" max'+inputErrorMessage lang Input { range = (Just min', _), step = Just step', inputType = ty, value = val } | ty == "date", Just x <- parseTime $ Txt.unpack val, Just y <- parseTime $ Txt.unpack min', Just z <- readMaybe' step', timeDiff x y `rem` toSeconds mempty { durationSeconds = 24*z } == Seconds 0 =- ("Must be in increments of " ++ Txt.unpack step' ++ " days from " ++- Txt.unpack min' ++ "!")+ i18n2 lang "err increments" step' min' | ty == "month" = "" -- Not prepared to properly validate this... | Just x <- parseTime $ Txt.unpack val, Just y <- parseTime $ Txt.unpack min', Just z <- readMaybe' step', timeDiff x y `rem` Seconds z == Seconds 0 =- ("Must be in increments of " ++ Txt.unpack step' ++ "s from " ++- Txt.unpack min' ++ "!")+ i18n2 lang "err increments" step' min' -- Validation specific to input types-inputErrorMessage self@Input { inputType = "color" }- | ("#[0-9a-fA-F]{6}" :: String) =~ value self = "Invalid colour value!"-inputErrorMessage self@Input { inputType = "date" } = isTime' self-inputErrorMessage self@Input { inputType = "datetime" } = isTime' self-inputErrorMessage self@Input { inputType = "datetime-local" } = isTime' self+inputErrorMessage lang self@Input { inputType = "color" }+ | ("#[0-9a-fA-F]{6}" :: String) =~ value self = i18n lang "err colour"+inputErrorMessage lang self@Input { inputType = "date" } = isTime' lang self+inputErrorMessage lang self@Input { inputType = "datetime" } = isTime' lang self+inputErrorMessage l self@Input { inputType = "datetime-local" } = isTime' l self -- This validation is less strict than many sites expect, but don't over-validate...-inputErrorMessage self@Input { inputType = "email" }- | '@' `Txt.elem` value self =- "Obviously invalid email address, needs an '@'!"-inputErrorMessage self@Input { inputType = "month" } = isTime' self-inputErrorMessage Input { inputType = "number", value = val }- | isNothing (readMaybe' val :: Maybe Float) = "Invalid number!"-inputErrorMessage Input { inputType = "range", value = val }- | isNothing (readMaybe' val :: Maybe Float) = "Invalid number!"-inputErrorMessage self@Input { inputType = "time" } = isTime' self-inputErrorMessage self@Input { inputType = "url" }- | isURL $ value self = "Invalid web address!"-inputErrorMessage self@Input { inputType = "week" } = isTime' self-inputErrorMessage _ = ""+inputErrorMessage lang self@Input { inputType = "email" }+ | '@' `Txt.elem` value self = i18n lang "err email"+inputErrorMessage lang self@Input { inputType = "month" } = isTime' lang self+inputErrorMessage lang Input { inputType = "number", value = val }+ | isNothing (readMaybe' val :: Maybe Float) = i18n lang "err number"+inputErrorMessage lang Input { inputType = "range", value = val }+ | isNothing (readMaybe' val :: Maybe Float) = i18n lang "err number"+inputErrorMessage lang self@Input { inputType = "time" } = isTime' lang self+inputErrorMessage lang self@Input { inputType = "url" }+ | isURL $ value self = i18n lang "err URL"+inputErrorMessage lang self@Input { inputType = "week" } = isTime' lang self+inputErrorMessage _ _ = "" -- | Describe why an input, once normalized, is invalid? Or returns empty string.-inputErrorMessage' :: Input -> [Char]-inputErrorMessage' = inputErrorMessage . normalizeInput+inputErrorMessage' :: String -> Input -> [Char]+inputErrorMessage' lang = inputErrorMessage lang . normalizeInput -- | Helper to parse the time stored in an input. parseTime :: String -> Maybe DateTime@@ -106,9 +103,9 @@ isTime :: Input -> Bool isTime = isJust . parseTime . Txt.unpack . value -- | Emit an error message if an input doesn't store a valid time.-isTime' :: Input -> String-isTime' x | isTime x = ""- | otherwise = "Invalid time format!"+isTime' :: String -> Input -> String+isTime' lang x | isTime x = ""+ | otherwise = i18n lang "err time" -- | Parse a Text into any type that can be parsed from strings. readMaybe' :: Read a => Txt.Text -> Maybe a readMaybe' = readMaybe . Txt.unpack
src/Text/HTML/Form/WebApp.hs view
@@ -182,18 +182,19 @@ _ -> toGVal () renderInput form ix input@Input { inputType = "color" } [] qs = template' "color.html" form ix input qs $ \x -> case x of- "colours" -> V.list $ L.map colourGVal tailwindColours+ "colours" -> V.list $ L.map colourGVal $ tailwindColours $ lang form "shades" -> toGVal False "subfolder" -> toGVal False _ -> toGVal () renderInput form ix input@Input { inputType = "color" } [c, ""] qs = template' "color.html" form ix input qs $ \x -> case x of- "colours" -> V.list $ L.map colourGVal tailwindColours- "shades" -> case Txt.unpack c `lookup` tailwindColours of+ "colours" -> V.list $ L.map colourGVal $ tailwindColours l+ "shades" -> case Txt.unpack c `lookup` tailwindColours l of Just shades -> V.list $ L.map shadeGVal shades Nothing -> toGVal False "subfolder" -> toGVal True _ -> toGVal ()+ where l = lang form renderInput form ix input [keyboard] qs = renderInput form ix input [keyboard, ""] qs renderInput form ix input [keyboard, ""] qs | Just (Just _) <- resolveSource path =
src/Text/HTML/Form/WebApp/Ginger.hs view
@@ -29,6 +29,7 @@ import Text.HTML.Form.Validate (inputErrorMessage') import Text.HTML.Form.Query (applyQuery)+import Text.HTML.Form.I18n (stringsJSON) -- | A key-value query string. type Query = [(ByteString, Maybe ByteString)]@@ -52,14 +53,16 @@ ctxt :: Text -> GVal (Run SourcePos (Writer Html) Html) ctxt "Q" = query2gval query ctxt "form" = form2gval form- ctxt "inputs" = list' $ Prelude.map (flip input2gval query) $+ ctxt "inputs" = list' $ Prelude.map (\x -> input2gval language x query) $ Prelude.zip [0..] $ inputs form- ctxt "input" = input2gval (ix, input) query+ ctxt "input" = input2gval language (ix, input) query ctxt "xURI" = fromFunction xURI+ ctxt "_" = toGVal $ stringsJSON language ctxt x = ctxt' x xURI [(_, uri)] = let uri' = Txt.unpack $ asText uri in return$toGVal$Txt.pack $ escapeURIString isUnescapedInURIComponent uri' xURI _ = return $ toGVal ()+ language = lang form -- | Lookup the given template from a compiled-in directory. resolveSource :: FilePath -> Maybe (Maybe [Char])@@ -97,11 +100,11 @@ ] -- | Convert an input to Ginger's datamodel.-input2gval :: (Int, Input) -> Query -> GVal m-input2gval (ix, input) query = orderedDict [+input2gval :: String -> (Int, Input) -> Query -> GVal m+input2gval language (ix, input) query = orderedDict [ "index" ~> ix, "label" ~> label input,- "error" ~> inputErrorMessage' (applyQuery input+ "error" ~> inputErrorMessage' language (applyQuery input [(B8.unpack k, B8.unpack $ fromMaybe "" v) | (k, v) <- query]), "description" ~> html (description input), "inputType" ~> inputType input,
+ tpl/base.html view
@@ -0,0 +1,71 @@+<!DOCTYPE html>+<html>+<head>+ <title>{{form.method}} {{form.action}}</title>+ <meta charset=utf-8 />+ <style>+ body { background: black; color: #eee; font: medium sans-serif; }+ h1 { text-align: center }+ aside { width: 20%; border-right: thin solid #eee; }+ dt { font-weight: bold; border-top: thin dashed #999; }+ dt a { color: inherit }+ .checked { color: lightgreen }+ .readonly { font-style: italic }+ .disabled { text-decoration: line-through }+ .selected { border-right: thick solid green }+ :link { color: white }++ input, select, textarea { display: none; }++ main { display: flex; flex-direction: row; }+ section { padding: 20px }++ nav { column-count: 4 } /* FIXME: Screen width issues? */+ nav dl { margin: 0 }+ dt, dd { break-inside: avoid }++ table td:not([colspan]) { width: 25% }+ table td { text-align: center }++ .error { color: red; }+ </style>+</head>+<body>+ <h1><strong>{{form.method}}</strong> <em>{{form.action}}</em></h1>+ <main>+ {% set selected=input.index %}+ <aside><dl>{% for input in inputs %}+ <dt class="{% if input.checked %}checked{% endif %} {% if input.index == selected %}selected{% endif %}">+ {% if input.checked %}✓{% endif %}+ {% if input.type == 'password' && input.value != '' %}✓{% endif %}+ {% if input.disabled %}{{ input.label }}+ {% else %}<a href="/{{input.index}}/{{Q}}" title="{{input.title}}">{{ input.label }}</a>+ {% endif %}</dt>+ {% if input.error %}<dd class="error">⚠️ {{ input.error }}</dd>{% endif %}+ {% if input.type != 'password' %}+ <dd class="{% if input.readonly %}readonly{% endif %}"+ {% if input.inputType == "color" %}style="background-color: {{ input.value }}"{% endif %}>+ {{ input.value }}+ </dd>+ {% endif %}+ {% endfor %}</dl></aside>++ {% block main %}+ {% if input.list %}<aside><dl>{% for grp in input.list %}+ <dt>{{ grp.label }}</dt>+ {% for opt in grp.opts %}+ {% if opt.disabled %}<dd>{{ opt.label }}</dd>+ {% else %}<dd>+ <a href="={{ opt.value|xURI }}{{Q}}" title="{{ opt.value }}">+ {{ opt.label }}</a>+ </dd>{% endif %}+ {% endfor %}+ {% endfor %}</dl></aside>{% endif %}++ {% block control %}<section>+ {{ input.description }}+ </section>{% endblock %}+ {% endblock %}+ </main>+</body>+</html>
+ tpl/cal/timezone.html view
@@ -0,0 +1,13 @@+{% extends "/base.html" %}++{%- block main -%}<section>+ <ul style="list-style: none">{% for cont in continents %}+ <li><a href="{{cont|xURI}}">{{ cont }}</a></li>+ {% endfor %}</ul>+</section>+<section>+ <h2><a href="..">{{ T.zone }}</a></h2>+ <ul style="display: flex; flex-wrap: wrap; justify-content: space-between; list-style: none">{% for tz in zones %}+ <li><a href="../zone={{tz.value}}">{{ tz.label }}</a> ({{ tz.offset }})</li>+ {% endfor %}</ul>+</section>{%- endblock -%}
+ tpl/cal/year-numpad.html view
@@ -0,0 +1,28 @@+{% extends "/base.html" %}++{%- block main -%}<section>+ <p>{{ T.year }}</p>+ <hr />+ <table>+ <tr>+ <td><a href=":7{{Q}}">7</a></td>+ <td><a href=":8{{Q}}">8</a></td>+ <td><a href=":9{{Q}}">9</a></td>+ </tr>+ <tr>+ <td><a href=":4{{Q}}">4</a></td>+ <td><a href=":5{{Q}}">5</a></td>+ <td><a href=":6{{Q}}">6</a></td>+ </tr>+ <tr>+ <td><a href=":1{{Q}}">1</a></td>+ <td><a href=":2{{Q}}">2</a></td>+ <td><a href=":3{{Q}}">3</a></td>+ </tr>+ <tr>+ <td><a href="-{{Q}}">{{ _["DEL"] }}</a></td>+ <td><a href=":0{{Q}}">0</a></td>+ <td><a href="..{{Q}}">✓</a></td>+ </tr>+ </table>+</section>{%- endblock -%}
+ tpl/checkbox.html view
@@ -0,0 +1,1 @@+{% extends "base.html" %}
+ tpl/color.html view
@@ -0,0 +1,21 @@+{% extends "base.html" %}++{%- block main -%}+ {% if shades %}<section>+ <ul style="list-style: none; background: linear-gradient(black, white, white)">+ {% for shade in shades %}+ <li><a href="={{ shade.value|xURI }}{{Q}}" style="color: {{ shade.value }}">+ {{ shade.label }}+ </a></li>+ {% endfor %}+ </ul>+ </section>{% endif %}+ <section>+ <ul style="list-style: none; display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px">+ {% for colour in colours %}+ <li><a href="{% if subfolder %}../{% endif %}{{ colour.label|xURI }}/={{ colour.value|xURI }}{{Q}}"+ style="color: {{ colour.value }}">{{ colour.label }}</a></li>+ {% endfor %}+ </ul>+ </section>+{%- endblock -%}
+ tpl/files.html view
@@ -0,0 +1,15 @@+{% extends "base.html" %}++{%- block main -%}+ <ul>{% for ancestor in path %}{% if ancestor %}+ <li><a href="{{ ancestor.link }}">{{ ancestor.name }}</a></li>+ {% endif %}{% endfor %}</ul>+ <ul>{% for dir in dirs %}+ <li><a href="{{ dir }}/">{{ dir }}</a></li>+ {% endfor %}</ul>+ <ul>{% for file in files %}+ <li><a href="/{{ input.index }}/={{ path|xURI }}{{ '/'|xURI }}{{ file|xURI }}">+ {{ file }}+ </a></li>+ {% endfor %}</ul>+{%- endblock -%}
+ tpl/gregorian.html view
@@ -0,0 +1,91 @@+{% extends "base.html" %}++{%- block control -%}<section>+ <style>+ .multi { display: flex }+ ul.control, .multi > ul {+ display: grid;+ gap: 5px;+ grid-template-columns: repeat(3, min-content);+ list-style-type: none;+ }+ </style>++ <p><a href="-year{{Q}}" title="Past year">↓</a>+ {% if input.inputType != "time" %}+ <a href="year/{{Q}}" title="Edit year">{{ T.year }}</a>+ <a href="+year{{Q}}" title="Next year">↑</a>++ <a href="-month{{Q}}" title="Past month">↓</a>+ {{ T.month }}+ <a href="+month{{Q}}" title="Next month">↑</a>+ {% endif %}++ {% if input.inputType != "month" %}+ {% if input.inputType != "time" %}+ <a href="-date{{Q}}" title="Past day">↓</a>+ {{ T.date }}+ <a href="+date{{Q}}" title="Next day">↑</a>+ {% endif %}++ {# NOTE: "week" handling is largely a matter of switching to +/- 7days #}+ {% if input.inputType != "date" && input.inputType != "week" %}+ <a href="-hour{{Q}}" title="Past hour">↓</a>+ {{ T.hour|pad2 }}+ <a href="+hour{{Q}}" title="Next hour">↑</a>+ :+ <a href="-minute{{Q}}" title="Past minute">↓</a>+ {{ T.minute|pad2 }}+ <a href="+minute{{Q}}" title="Next minute">↑</a>+ <a href="meridiem{{Q}}" title="Toggle AM/PM">{{ T.meridiem }}</a>++ {% if input.inputType != "datetime-local" && input.inputType != "time" %}+ <a href="-zone{{Q}}" title="Previous timezone">↓</a>+ <a href="zone/{{Q}}">{{ T.zone }}</a>+ <a href="+zone{{Q}}" title="Next zone">↑</a></p>+ {% endif %}+ {% endif %}+ {% endif %}+ <p><a href="now{{Q}}" title="Select current date & time">Now</a></a>++ <div class="multi">+ {% if input.inputType != "time" %}+ <ul>+ <li><a href="month=0{{Q}}">{{ _["January"] }}</a></li>+ <li><a href="month=1{{Q}}">{{ _["February"] }}</a></li>+ <li><a href="month=2{{Q}}">{{ _["March"] }}</a></li>+ <li><a href="month=3{{Q}}">{{ _["April"] }}</a></li>+ <li><a href="month=4{{Q}}">{{ _["May"] }}</a></li>+ <li><a href="month=5{{Q}}">{{ _["June"] }}</a></li>+ <li><a href="month=6{{Q}}">{{ _["July"] }}</a></li>+ <li><a href="month=7{{Q}}">{{ _["August"] }}</a></li>+ <li><a href="month=8{{Q}}">{{ _["September"] }}</a></li>+ <li><a href="month=9{{Q}}">{{ _["October"] }}</a></li>+ <li><a href="month=10{{Q}}">{{ _["November"] }}</a></li>+ <li><a href="month=11{{Q}}">{{ _["December"] }}</a></li>+ </ul>+ {% endif %}++ {% if input.inputType != "month" %}+ {# TODO: Do we want to refine this for "week" inputs? #}+ {% if input.inputType != "time" %}+ <ul style="grid-template-columns: repeat(7, min-content)">+ {% for i in 1|seqTo(T.daysInMonth) %}+ <li {% if i == 1 %}style="grid-column: {{ T.monthStart }}"{% endif %}>+ <a href="date={{ i }}{{Q}}">{{ i }}</a>+ </li>+ {% endfor %}+ </ul>+ {% endif %}++ {% if input.inputType != "date" && input.inputType != "week" %}+ <ul>{% for i in 1|seqTo(12) %}+ <li><a href="hour={{ i }}{{Q}}">{{ i|pad2 }}:{{ T.minute|pad2 }}</a></li>+ {% endfor %}</ul>+ <ul>{% for i in 0|seqTo(5, 55) %}+ <li><a href="minute={{ i }}{{Q}}">{{ T.hour|pad2 }}:{{ i|pad2 }}</a></li>+ {% endfor %}</ul>+ {% endif %}+ {% endif %}+ </div>+</section>{%- endblock -%}
+ tpl/image-button.html view
@@ -0,0 +1,15 @@+{% extends "base.html" %}++{# NOTE: This is pretty much a legacy input type now.+ We should be sending x/y coordinates to the server,+ but that'd be frustrating to operate using e.g. a TV remote. #}++{%- block main -%}<section>+ <img src="{{ input.src }}" alt="{{ input.alt }}" title="{{ input.alt }}" />+ <div>{{ input.description }}</div>+ <hr />+ <!-- TODO: Internationalize! -->+ <p><a href="_{{Q}}">{{ _["Upload Link"] }}</a> {{ _["Upload Suffix"] }}+ <code>{{ input.form.action|default(form.action) }}+ ({{ input.form.method|default(form.method) }})</code></p>+</section>{%- endblock -%}
+ tpl/keyboards/latin1.html view
@@ -0,0 +1,78 @@+{% extends "/base.html" %}++{%- block control -%}<section>+ {% if input.type != 'password' %}+ <p><span style="white-space: pre">{{ input.value }}</span>+ {% if input.multiple %}<a href=":%0A{{Q}}">↲</a>{% endif %}</p>+ <hr />+ {% endif %}+ <p><strong>abc</strong> | <a href="latin1-upper/{{Q}}">ABC</a> |+ <a href="latin1-symbol/{{Q}}">!@#</a> |+ <a href="latin1-accent-lower/{{Q}}">àêï</a> |+ <a href="latin1-accent/{{Q}}">ÀÊÏ</a></p>+ <table>+ <tr>+ <td><a href=":a{{Q}}">a</a></td>+ <td><a href=":b{{Q}}">b</a></td>+ <td><a href=":c{{Q}}">c</a></td>+ <td><a href=":d{{Q}}">d</a></td>+ <td><a href=":e{{Q}}">e</a></td>+ <td><a href=":f{{Q}}">f</a></td>+ </tr>+ <tr>+ <td><a href=":g{{Q}}">g</a></td>+ <td><a href=":h{{Q}}">h</a></td>+ <td><a href=":i{{Q}}">i</a></td>+ <td><a href=":j{{Q}}">j</a></td>+ <td><a href=":k{{Q}}">k</a></td>+ <td><a href=":l{{Q}}">l</a></td>+ </tr>+ <tr>+ <td><a href=":m{{Q}}">m</a></td>+ <td><a href=":n{{Q}}">n</a></td>+ <td><a href=":o{{Q}}">o</a></td>+ <td><a href=":p{{Q}}">p</a></td>+ <td><a href=":q{{Q}}">q</a></td>+ <td><a href=":r{{Q}}">r</a></td>+ </tr>+ <tr>+ <td><a href=":s{{Q}}">s</a></td>+ <td><a href=":t{{Q}}">t</a></td>+ <td><a href=":u{{Q}}">u</a></td>+ <td><a href=":v{{Q}}">v</a></td>+ <td><a href=":w{{Q}}">w</a></td>+ <td><a href=":x{{Q}}">x</a></td>+ </tr>+ <tr>+ <td><a href=":y{{Q}}">y</a></td>+ <td><a href=":z{{Q}}">z</a></td>+ <td><a href=":1{{Q}}">1</a></td>+ <td><a href=":2{{Q}}">2</a></td>+ <td><a href=":3{{Q}}">3</a></td>+ <td><a href=":4{{Q}}">4</a></td>+ </tr>+ <tr>+ <td><a href=":5{{Q}}">5</a></td>+ <td><a href=":6{{Q}}">6</a></td>+ <td><a href=":7{{Q}}">7</a></td>+ <td><a href=":8{{Q}}">8</a></td>+ <td><a href=":9{{Q}}">9</a></td>+ <td><a href=":0{{Q}}">0</a></td>+ </tr>+ {% if input.inputType == "email" || input.inputType == "url" %}<tr>+ {% if input.inputType == "email" %}+ <td><a href=":@{{Q}}">@</a></td>+ {% else %}+ <td><a href=":{{ '/'|xURI }}{{Q}}">/</a></td>+ {% endif %}+ <td><a href=":.{{Q}}">.</a></td>+ <td colspan="2"><a href=":.com{{Q}}">.com</a>+ <td colspan="2"><a href=":.org{{Q}}">.org</a>+ </tr>{% endif %}+ <tr>+ <td colspan="2"><a href=":%20{{Q}}">{{ _["SPACE"] }}</a></td>+ <td colspan="2"><a href="-{{Q}}">{{ _["DEL"] }}</a></td>+ <td colspan="2"><a href="={{Q}}">{{ _["CLEAR"] }}</a></td>+ </tr>+ </table>+</section>{%- endblock -%}
+ tpl/keyboards/latin1/latin1-accent-lower.html view
@@ -0,0 +1,70 @@+{% extends "/base.html" %}++{%- block control -%}<section>+ {% if input.type != 'password' %}+ <p><span style="white-space: pre">{{ input.value }}</span>+ {% if input.multiple %}<a href=":%0A{{Q}}">↲</a>{% endif %}</p>+ <hr />+ {% endif %}+ <p><a href="..">abc</a> | <a href="../latin1-upper/{{Q}}">ABC</a> |+ <a href="../latin1-symbol/{{Q}}">!@#</a> | <strong>àêï</strong> |+ <a href="../latin1-accent/{{Q}}">ÀÊÏ</a></p>+ <table>+ <tr>+ <td><a href=":{{'à'|xURI}}{{Q}}">à</a></td>+ <td><a href=":{{'á'|xURI}}{{Q}}">á</a></td>+ <td><a href=":{{'â'|xURI}}{{Q}}">â</a></td>+ <td><a href=":{{'ä'|xURI}}{{Q}}">ä</a></td>+ <td><a href=":{{'ã'|xURI}}{{Q}}">ã</a></td>+ <td><a href=":{{'å'|xURI}}{{Q}}">å</a></td>+ </tr>+ <tr>+ <td><a href=":{{'è'|xURI}}{{Q}}">è</a></td>+ <td><a href=":{{'é'|xURI}}{{Q}}">é</a></td>+ <td><a href=":{{'ê'|xURI}}{{Q}}">ê</a></td>+ <td><a href=":{{'ë'|xURI}}{{Q}}">ë</a></td>+ <td><a href=":{{'ç'|xURI}}{{Q}}">ç</a></td>+ <td><a href=":{{'æ'|xURI}}{{Q}}">æ</a></td>+ </tr>+ <tr>+ <td><a href=":{{'ì'|xURI}}{{Q}}">ì</a></td>+ <td><a href=":{{'í'|xURI}}{{Q}}">í</a></td>+ <td><a href=":{{'î'|xURI}}{{Q}}">î</a></td>+ <td><a href=":{{'ï'|xURI}}{{Q}}">ï</a></td>+ <td><a href=":{{'ð'|xURI}}{{Q}}">ð</a></td>+ <td><a href=":{{'ñ'|xURI}}{{Q}}">ñ</a></td>+ </tr>+ <tr>+ <td><a href=":{{'ò'|xURI}}{{Q}}">ò</a></td>+ <td><a href=":{{'ó'|xURI}}{{Q}}">ó</a></td>+ <td><a href=":{{'ô'|xURI}}{{Q}}">ô</a></td>+ <td><a href=":{{'ö'|xURI}}{{Q}}">ö</a></td>+ <td><a href=":{{'õ'|xURI}}{{Q}}">õ</a></td>+ <td><a href=":{{'ø'|xURI}}{{Q}}">ø</a></td>+ </tr>+ <tr>+ <td><a href=":{{'ù'|xURI}}{{Q}}">ù</a></td>+ <td><a href=":{{'ú'|xURI}}{{Q}}">ú</a></td>+ <td><a href=":{{'û'|xURI}}{{Q}}">û</a></td>+ <td><a href=":{{'ü'|xURI}}{{Q}}">ü</a></td>+ <td><a href=":{{'ý'|xURI}}{{Q}}">ý</a></td>+ <td><a href=":{{'ÿ'|xURI}}{{Q}}">ÿ</a></td>+ </tr>+ {% if input.inputType == "email" || input.inputType == "url" %}<tr>+ {% if input.inputType == "email" %}+ <td><a href=":@{{Q}}">@</a></td>+ {% else %}+ <td><a href=":{{ '/'|xURI }}{{Q}}">/</a></td>+ {% endif %}+ <td><a href=":.{{Q}}">.</a></td>+ <td colspan="2"><a href=":.com{{Q}}">.com</a>+ <td colspan="2"><a href=":.org{{Q}}">.org</a>+ </tr>{% endif %}+ <tr>+ <td><a href=":{{'þ'|xURI}}{{Q}}">þ</a></td>+ <td><a href=":%20{{Q}}">{{ _["SPACE"] }}</a></td>+ <td colspan="2"><a href="-{{Q}}">{{ _["DEL"] }}</a></td>+ <td colspan="2"><a href="={{Q}}">{{ _["CLEAR"] }}</a></td>+ </tr>+ </table>+</section>{%- endblock -%}
+ tpl/keyboards/latin1/latin1-accent.html view
@@ -0,0 +1,71 @@+{% extends "/base.html" %}++{%- block control -%}<section>+ {% if input.type != 'password' %}+ <p><span style="white-space: pre">{{ input.value }}</span>+ {% if input.multiple %}<a href=":%0A{{Q}}">↲</a>{% endif %}</p>+ <hr />+ {% endif %}+ <p><a href="..">abc</a> | <a href="../latin1-upper/{{Q}}">ABC</a> |+ <a href="../latin1-symbol/{{Q}}">!@#</a> |+ <a href="../latin1-accent-lower/{{Q}}">àêï</a> |+ <strong>ÀÊÏ</strong></p>+ <table>+ <tr>+ <td><a href=":{{'À'|xURI}}{{Q}}">À</a></td>+ <td><a href=":{{'Á'|xURI}}{{Q}}">Á</a></td>+ <td><a href=":{{'Â'|xURI}}{{Q}}">Â</a></td>+ <td><a href=":{{'Ä'|xURI}}{{Q}}">Ä</a></td>+ <td><a href=":{{'Ã'|xURI}}{{Q}}">Ã</a></td>+ <td><a href=":{{'Å'|xURI}}{{Q}}">Å</a></td>+ </tr>+ <tr>+ <td><a href=":{{'È'|xURI}}{{Q}}">È</a></td>+ <td><a href=":{{'É'|xURI}}{{Q}}">É</a></td>+ <td><a href=":{{'Ê'|xURI}}{{Q}}">Ê</a></td>+ <td><a href=":{{'Ë'|xURI}}{{Q}}">Ë</a></td>+ <td><a href=":{{'Ç'|xURI}}{{Q}}">Ç</a></td>+ <td><a href=":{{'Æ'|xURI}}{{Q}}">Æ</a></td>+ </tr>+ <tr>+ <td><a href=":{{'Ì'|xURI}}{{Q}}">Ì</a></td>+ <td><a href=":{{'Í'|xURI}}{{Q}}">Í</a></td>+ <td><a href=":{{'Î'|xURI}}{{Q}}">Î</a></td>+ <td><a href=":{{'Ï'|xURI}}{{Q}}">Ï</a></td>+ <td><a href=":{{'Ð'|xURI}}{{Q}}">Ð</a></td>+ <td><a href=":{{'Ñ'|xURI}}{{Q}}">Ñ</a></td>+ </tr>+ <tr>+ <td><a href=":{{'Ò'|xURI}}{{Q}}">Ò</a></td>+ <td><a href=":{{'Ó'|xURI}}{{Q}}">Ó</a></td>+ <td><a href=":{{'Ô'|xURI}}{{Q}}">Ô</a></td>+ <td><a href=":{{'Ö'|xURI}}{{Q}}">Ö</a></td>+ <td><a href=":{{'Õ'|xURI}}{{Q}}">Õ</a></td>+ <td><a href=":{{'Ø'|xURI}}{{Q}}">Ø</a></td>+ </tr>+ <tr>+ <td><a href=":{{'Ù'|xURI}}{{Q}}">Ù</a></td>+ <td><a href=":{{'Ú'|xURI}}{{Q}}">Ú</a></td>+ <td><a href=":{{'Û'|xURI}}{{Q}}">Û</a></td>+ <td><a href=":{{'Ü'|xURI}}{{Q}}">Ü</a></td>+ <td><a href=":{{'Ý'|xURI}}{{Q}}">Ý</a></td>+ <td><a href=":{{'ß'|xURI}}{{Q}}">ß</a></td>+ </tr>+ {% if input.inputType == "email" || input.inputType == "url" %}<tr>+ {% if input.inputType == "email" %}+ <td><a href=":@{{Q}}">@</a></td>+ {% else %}+ <td><a href=":{{ '/'|xURI }}{{Q}}">/</a></td>+ {% endif %}+ <td><a href=":.{{Q}}">.</a></td>+ <td colspan="2"><a href=":.com{{Q}}">.com</a>+ <td colspan="2"><a href=":.org{{Q}}">.org</a>+ </tr>{% endif %}+ <tr>+ <td><a href=":{{'Þ'|xURI}}{{Q}}">Þ</a></td>+ <td><a href=":%20{{Q}}">{{ _["SPACE"] }}</a></td>+ <td colspan="2"><a href="-{{Q}}">{{ _["DEL"] }}</a></td>+ <td colspan="2"><a href="={{Q}}">{{ _["CLEAR"] }}</a></td>+ </tr>+ </table>+</section>{%- endblock -%}
+ tpl/keyboards/latin1/latin1-symbol.html view
@@ -0,0 +1,111 @@+{% extends "/base.html" %}++{%- block control -%}<section>+ {% if input.type != 'password' %}+ <p><span style="white-space: pre">{{ input.value }}</span>+ {% if input.multiple %}<a href=":%0A{{Q}}">↲</a>{% endif %}</p>+ <hr />+ {% endif %}+ <p><a href="..{{Q}}">abc</a> | <a href="../latin1-upper/{{Q}}">ABC</a> |+ <strong>!@#</strong> | <a href="../latin1-accent-lower/{{Q}}">àêï</a> |+ <a href="../latin1-accent/{{Q}}">ÀÊÏ</a></p>+ <table>+ <tr>+ <td><a href=":{{'?'|xURI}}{{Q}}">?</a></td>+ <td><a href=":,{{Q}}">,</a></td>+ <td><a href=":.{{Q}}">.</a></td>+ <td><a href="::{{Q}}">:</a></td>+ <td><a href=":;{{Q}}">;</a></td>+ <td><a href=":{{'('|xURI}}{{Q}}">(</a></td>+ <td><a href=":{{')'|xURI}}{{Q}}">)</a></td>+ </tr>+ <tr>+ <td><a href=":{{'/'|xURI}}{{Q}}">/</a></td>+ <td><a href=":{{'='|xURI}}{{Q}}">=</a></td>+ <td><a href=':"{{Q}}'>"</a></td>+ <td><a href=":'{{Q}}">'</a></td>+ <td><a href=":{{'!'|xURI}}{{Q}}">!</a></td>+ <td><a href=":{{'['|xURI}}{{Q}}">[</a></td>+ <td><a href=":{{']'|xURI}}{{Q}}">]</a></td>+ </tr>+ <tr>+ <td><a href=":\{{Q}}">\</a></td>+ <td><a href=":{{'~'|xURI}}{{Q}}">~</a></td>+ <td><a href=":{{'`'|xURI}}{{Q}}">`</a></td>+ <td><a href=":{{'|'|xURI}}{{Q}}">|</a></td>+ <td><a href=":{{'@'|xURI}}{{Q}}">@</a></td>+ <td><a href=":{{'{'|xURI}}{{Q}}">{</a></td>+ <td><a href=":{{'}'|xURI}}{{Q}}">}</a></td>+ </tr>+ <tr>+ <td><a href=":{{'¸'|xURI}}{{Q}}">¸</a></td>+ <td><a href=":{{'%'|xURI}}{{Q}}">%</a></td>+ <td><a href=":{{'^'|xURI}}{{Q}}">^</a></td>+ <td><a href=":{{'&'|xURI}}{{Q}}">&</a></td>+ <td><a href=":{{'*'|xURI}}{{Q}}">*</a></td>+ <td><a href=":{{'<'|xURI}}{{Q}}"><</a></td>+ <td><a href=":{{'>'|xURI}}{{Q}}">></a></td>+ </tr>+ <tr>+ <td><a href=":{{'¶'|xURI}}{{Q}}">¶</a></td>+ <td><a href=":{{'#'|xURI}}{{Q}}">#</a></td>+ <td><a href=":{{'$'|xURI}}{{Q}}">$</a></td>+ <td><a href=":{{'-'|xURI}}{{Q}}">-</a></td>+ <td><a href=":{{'_'|xURI}}{{Q}}">_</a></td>+ <td><a href=":{{'´'|xURI}}{{Q}}">´</a></td>+ <td><a href=":{{'·'|xURI}}{{Q}}">·</a></td>+ </tr>+ <tr>+ <td><a href=":{{'×'|xURI}}{{Q}}">×</a></td>+ <td><a href=":{{'¢'|xURI}}{{Q}}">¢</a></td>+ <td><a href=":{{'£'|xURI}}{{Q}}">£</a></td>+ <td><a href=":{{'¤'|xURI}}{{Q}}">¤</a></td>+ <td><a href=":{{'¥'|xURI}}{{Q}}">¥</a></td>+ <td><a href=":{{'«'|xURI}}{{Q}}">«</a></td>+ <td><a href=":{{'»'|xURI}}{{Q}}">»</a></td>+ </tr>+ <tr>+ <td><a href=":{{'÷'|xURI}}{{Q}}">÷</a></td>+ <td><a href=":{{'§'|xURI}}{{Q}}">§</a></td>+ <td><a href=":{{'¨'|xURI}}{{Q}}">¨</a></td>+ <td><a href=":{{'©'|xURI}}{{Q}}">©</a></td>+ <td><a href=":{{'ª'|xURI}}{{Q}}">ª</a></td>+ <td><a href=":{{'¡'|xURI}}{{Q}}">¡</a></td>+ <td><a href=":{{'¦'|xURI}}{{Q}}">¦</a></td>+ </tr>+ <tr>+ <td><a href=":{{'µ'|xURI}}{{Q}}">µ</a></td>+ <td><a href=":{{'¬'|xURI}}{{Q}}">¬</a></td>+ <td><a href=":{{'¯'|xURI}}{{Q}}">¯</a></td>+ <td><a href=":{{'®'|xURI}}{{Q}}">®</a></td>+ <td><a href=":{{'°'|xURI}}{{Q}}">°</a></td>+ <td><a href=":{{'±'|xURI}}{{Q}}">±</a></td>+ <td><a href=":{{'¿'|xURI}}{{Q}}">¿</a></td>+ </tr>+ <tr>+ <td><a href=":{{'º'|xURI}}{{Q}}">º</a></td>+ <td><a href=":{{'¹'|xURI}}{{Q}}">¹</a></td>+ <td><a href=":{{'²'|xURI}}{{Q}}">²</a></td>+ <td><a href=":{{'³'|xURI}}{{Q}}">³</a></td>+ <td><a href=":{{'¼'|xURI}}{{Q}}">¼</a></td>+ <td><a href=":{{'½'|xURI}}{{Q}}">½</a></td>+ <td><a href=":{{'¾'|xURI}}{{Q}}">¾</a></td>+ </tr>+ {% if input.inputType == "email" || input.inputType == "url" %}<tr>+ {% if input.inputType == "email" %}+ <td><a href=":@{{Q}}">@</a></td>+ {% else %}+ <td><a href=":{{ '/'|xURI }}{{Q}}">/</a></td>+ {% endif %}+ <td><a href=":.{{Q}}">.</a></td>+ <td colspan="2"><a href=":.com{{Q}}">.com</a>+ <td colspan="2"><a href=":.org{{Q}}">.org</a>+ </tr>{% endif %}+ <tr>+ <td><a href=":{{'+'|xURI}}{{Q}}">+</a></td>+ <td colspan="2"><a href=":%20{{Q}}">{{ _["SPACE"] }}</a></td>+ <td colspan="2"><a href="-{{Q}}">{{ _["DEL"] }}</a></td>+ <td colspan="2"><a href="={{Q}}">{{ _["CLEAR"] }}</a></td>+ </tr>+ </table>+</section>{%- endblock -%}
+ tpl/keyboards/latin1/latin1-upper.html view
@@ -0,0 +1,78 @@+{% extends "/base.html" %}++{%- block control -%}<section>+ {% if input.type != 'password' %}+ <p><span style="white-space: pre">{{ input.value }}</span>+ {% if input.multiple %}<a href=":%0A{{Q}}">↲</a>{% endif %}</p>+ <hr />+ {% endif %}+ <p><a href="..{{Q}}">abc</a> | <strong>ABC</strong> |+ <a href="../latin1-symbol/{{Q}}">!@#</a> |+ <a href="../latin1-accent-lower/{{Q}}">àêï</a> |+ <a href="../latin1-accent/{{Q}}">ÀÊÏ</a></p>+ <table>+ <tr>+ <td><a href=":A{{Q}}">A</a></td>+ <td><a href=":B{{Q}}">B</a></td>+ <td><a href=":C{{Q}}">C</a></td>+ <td><a href=":D{{Q}}">D</a></td>+ <td><a href=":E{{Q}}">E</a></td>+ <td><a href=":F{{Q}}">F</a></td>+ </tr>+ <tr>+ <td><a href=":G{{Q}}">G</a></td>+ <td><a href=":H{{Q}}">H</a></td>+ <td><a href=":I{{Q}}">I</a></td>+ <td><a href=":J{{Q}}">J</a></td>+ <td><a href=":K{{Q}}">K</a></td>+ <td><a href=":L{{Q}}">L</a></td>+ </tr>+ <tr>+ <td><a href=":M{{Q}}">M</a></td>+ <td><a href=":N{{Q}}">N</a></td>+ <td><a href=":O{{Q}}">O</a></td>+ <td><a href=":P{{Q}}">P</a></td>+ <td><a href=":Q{{Q}}">Q</a></td>+ <td><a href=":R{{Q}}">R</a></td>+ </tr>+ <tr>+ <td><a href=":S{{Q}}">S</a></td>+ <td><a href=":T{{Q}}">T</a></td>+ <td><a href=":U{{Q}}">U</a></td>+ <td><a href=":V{{Q}}">V</a></td>+ <td><a href=":W{{Q}}">W</a></td>+ <td><a href=":X{{Q}}">X</a></td>+ </tr>+ <tr>+ <td><a href=":Y{{Q}}">Y</a></td>+ <td><a href=":Z{{Q}}">Z</a></td>+ <td><a href=":1{{Q}}">1</a></td>+ <td><a href=":2{{Q}}">2</a></td>+ <td><a href=":3{{Q}}">3</a></td>+ <td><a href=":4{{Q}}">4</a></td>+ </tr>+ <tr>+ <td><a href=":5{{Q}}">5</a></td>+ <td><a href=":6{{Q}}">6</a></td>+ <td><a href=":7{{Q}}">7</a></td>+ <td><a href=":8{{Q}}">8</a></td>+ <td><a href=":9{{Q}}">9</a></td>+ <td><a href=":0{{Q}}">0</a></td>+ </tr>+ {% if input.inputType == "email" || input.inputType == "url" %}<tr>+ {% if input.inputType == "email" %}+ <td><a href=":@{{Q}}">@</a></td>+ {% else %}+ <td><a href=":{{ '/'|xURI }}{{Q}}">/</a></td>+ {% endif %}+ <td><a href=":.{{Q}}">.</a></td>+ <td colspan="2"><a href=":.com{{Q}}">.com</a>+ <td colspan="2"><a href=":.org{{Q}}">.org</a>+ </tr>{% endif %}+ <tr>+ <td colspan="2"><a href=":%20{{Q}}">{{ _["SPACE"] }}</a></td>+ <td colspan="2"><a href="-{{Q}}">{{ _["DEL"] }}</a></td>+ <td colspan="2"><a href="={{Q}}">{{ _["CLEAR"] }}</a></td>+ </tr>+ </table>+</section>{%- endblock -%}
+ tpl/number.html view
@@ -0,0 +1,34 @@+{% extends "base.html" %}++{%- block main -%}<section>+ <p><a href="+-{{input.step|default(1)}}{{Q}}">↓</a>+ {{ input.value }}+ <a href="+{{input.step|default(1)}}{{Q}}">↑</a></p>+ <hr />+ <table>+ <tr>+ <td><a href=":7{{Q}}">7</a></td>+ <td><a href=":8{{Q}}">8</a></td>+ <td><a href=":9{{Q}}">9</a></td>+ <td><a href=":-{{Q}}">-</a></td>+ </tr>+ <tr>+ <td><a href=":4{{Q}}">4</a></td>+ <td><a href=":5{{Q}}">5</a></td>+ <td><a href=":6{{Q}}">6</a></td>+ <td><a href=":%20{{Q}}">{{ _["SPACE"] }}</a></td>+ </tr>+ <tr>+ <td><a href=":1{{Q}}">1</a></td>+ <td><a href=":2{{Q}}">2</a></td>+ <td><a href=":3{{Q}}">3</a></td>+ <td><a href="-{{Q}}">{{ _["DEL"] }}</a></td>+ </tr>+ <tr>+ <td><a href=":.{{Q}}">.</a></td>+ <td><a href=":0{{Q}}">0</a></td>+ <td><a href=":,{{Q}}">,</a></td>+ <td><a href="={{Q}}">{{ _["CLEAR"] }}</a></td>+ </tr>+ </table>+</section>{%- endblock -%}
+ tpl/radio.html view
@@ -0,0 +1,1 @@+{% extends "base.html" %}
+ tpl/reset.html view
@@ -0,0 +1,7 @@+{% extends "base.html" %}++{%- block main -%}<section>+ <div>{{ input.description }}</div>+ <hr />+ <p><a href="_">{{ _["Restore defaults"] }}</a></p>+</section>{%- endblock -%}
+ tpl/select.html view
@@ -0,0 +1,32 @@+{% extends "base.html" %}++{%- block main -%}<section>+ <h2>{{ input.label }}</h2>+ {{ input.description }}+ <hr />+ <nav><dl>{% for grp in input.list %}+ {% if grp.label == "" %}{% for opt in grp.opts %}+ {% if opt.disabled %}+ <dt>{{ opt.label }}</dt>+ {% else %}+ <dt {% if opt.selected %}class="checked"{% endif %}>+ {% if opt.selected %}✓{% endif %}+ <a href="={{opt.value|xURI}}{{Q}}" title="{{opt.value}}">{{opt.label}}</a>+ </dt>+ {% endif %}+ {% endfor %}{% else %}+ <dt>{{ opt.label }}</dt>+ {% for opt in grp.opts %}+ {% if opt.disabled %}+ <dd>{{ opt.label }}</dd>+ {% else %}+ <dd {% if opt.selected %}class="checked"{% endif %}>+ {% if opt.selected %}✓{% endif %}+ <a href="={{opt.value|xURI}}{{Q}}" title="{{opt.value}}">+ {{opt.label}}</a>+ </dd>+ {% endif %}+ {% endfor %}+ {% endif %}+ {% endfor %}</dl></nav>+</section>{%- endblock -%}
+ tpl/submit.html view
@@ -0,0 +1,14 @@+{% extends "base.html" %}++{%- block main -%}<section>+ <div>{{ input.description }}</div>+ <hr />+ {% if isFormValid %}+ <!-- TODO: Internationalize! -->+ <p><a href="_{{Q}}">Upload</a> to+ <code>{{ input.form.action|default(form.action) }}+ ({{ input.form.method|default(form.method) }})</code></p>+ {% else %}+ <p>{{ _["errored"] }}</p>+ {% endif %}+</section>{%- endblock -%}
+ tpl/tel.html view
@@ -0,0 +1,32 @@+{% extends "base.html" %}++{%- block control -%}<section>+ <p>{{ input.value }}</p>+ <hr />+ <table>+ <tr>+ <td><a href=":7{{Q}}">7</a></td>+ <td><a href=":8{{Q}}">8</a></td>+ <td><a href=":9{{Q}}">9</a></td>+ <td><a href=":-{{Q}}">-</a></td>+ </tr>+ <tr>+ <td><a href=":4{{Q}}">4</a></td>+ <td><a href=":5{{Q}}">5</a></td>+ <td><a href=":6{{Q}}">6</a></td>+ <td><a href=":%20{{Q}}">{{ _["SPACE"] }}</a></td>+ </tr>+ <tr>+ <td><a href=":1{{Q}}">1</a></td>+ <td><a href=":2{{Q}}">2</a></td>+ <td><a href=":3{{Q}}">3</a></td>+ <td><a href="-{{Q}}">{{ _["DEL"] }}</a></td>+ </tr>+ <tr>+ <td><a href=":*{{Q}}">*</a></td>+ <td><a href=":0{{Q}}">0</a></td>+ <td><a href=":#{{Q}}">#</a></td>+ <td><a href=":.{{Q}}">.</a></td>+ </tr>+ </table>+</section>{%- endblock -%}