From 2bd0c78583cc445e8dc9f4858603f16570bb33e3 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 4 May 2018 19:30:58 -0400 Subject: [PATCH] Add some parsing stuff to finished/ for Tag --- finished/elm-package.json | 1 + finished/elm-stuff/exact-dependencies.json | 2 + .../parser-primitives/1.0.0/.gitignore | 1 + .../elm-tools/parser-primitives/1.0.0/LICENSE | 27 + .../parser-primitives/1.0.0/README.md | 7 + .../parser-primitives/1.0.0/elm-package.json | 17 + .../1.0.0/src/Native/ParserPrimitives.js | 130 ++ .../1.0.0/src/ParserPrimitives.elm | 109 ++ .../elm-tools/parser/2.0.1/.gitignore | 1 + .../packages/elm-tools/parser/2.0.1/LICENSE | 27 + .../packages/elm-tools/parser/2.0.1/README.md | 170 +++ .../elm-tools/parser/2.0.1/comparison.md | 69 + .../elm-tools/parser/2.0.1/elm-package.json | 19 + .../elm-tools/parser/2.0.1/src/Parser.elm | 1116 +++++++++++++++++ .../parser/2.0.1/src/Parser/Internal.elm | 149 +++ .../parser/2.0.1/src/Parser/LanguageKit.elm | 509 ++++++++ .../parser/2.0.1/src/Parser/LowLevel.elm | 118 ++ finished/src/Data/Article.elm | 21 - finished/src/Data/Article/Tag.elm | 55 + finished/src/Page/Article/Editor.elm | 43 +- finished/src/Page/Home.elm | 4 +- finished/src/Request/Article.elm | 11 +- finished/src/Request/Article/Comments.elm | 2 +- finished/src/Views/Article/Feed.elm | 5 +- 24 files changed, 2565 insertions(+), 48 deletions(-) create mode 100644 finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/.gitignore create mode 100644 finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/LICENSE create mode 100644 finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/README.md create mode 100644 finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/elm-package.json create mode 100644 finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/src/Native/ParserPrimitives.js create mode 100644 finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/src/ParserPrimitives.elm create mode 100644 finished/elm-stuff/packages/elm-tools/parser/2.0.1/.gitignore create mode 100644 finished/elm-stuff/packages/elm-tools/parser/2.0.1/LICENSE create mode 100644 finished/elm-stuff/packages/elm-tools/parser/2.0.1/README.md create mode 100644 finished/elm-stuff/packages/elm-tools/parser/2.0.1/comparison.md create mode 100644 finished/elm-stuff/packages/elm-tools/parser/2.0.1/elm-package.json create mode 100644 finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser.elm create mode 100644 finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/Internal.elm create mode 100644 finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/LanguageKit.elm create mode 100644 finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/LowLevel.elm create mode 100644 finished/src/Data/Article/Tag.elm diff --git a/finished/elm-package.json b/finished/elm-package.json index 7c979f1..bc1d227 100644 --- a/finished/elm-package.json +++ b/finished/elm-package.json @@ -15,6 +15,7 @@ "elm-lang/html": "2.0.0 <= v < 3.0.0", "elm-lang/http": "1.0.0 <= v < 2.0.0", "elm-lang/navigation": "2.1.0 <= v < 3.0.0", + "elm-tools/parser": "2.0.1 <= v < 3.0.0", "evancz/elm-markdown": "3.0.2 <= v < 4.0.0", "evancz/url-parser": "2.0.1 <= v < 3.0.0", "lukewestby/elm-http-builder": "5.1.0 <= v < 6.0.0", diff --git a/finished/elm-stuff/exact-dependencies.json b/finished/elm-stuff/exact-dependencies.json index 77b48b4..2e36c57 100644 --- a/finished/elm-stuff/exact-dependencies.json +++ b/finished/elm-stuff/exact-dependencies.json @@ -1,6 +1,7 @@ { "rtfeldman/elm-validate": "3.0.0", "rtfeldman/selectlist": "1.0.0", + "elm-tools/parser-primitives": "1.0.0", "elm-lang/navigation": "2.1.0", "elm-lang/virtual-dom": "2.0.4", "evancz/url-parser": "2.0.1", @@ -8,6 +9,7 @@ "evancz/elm-markdown": "3.0.2", "elm-lang/dom": "1.1.1", "elm-lang/html": "2.0.0", + "elm-tools/parser": "2.0.1", "elm-community/json-extra": "2.7.0", "elm-lang/http": "1.0.0", "lukewestby/elm-http-builder": "5.2.0", diff --git a/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/.gitignore b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/.gitignore new file mode 100644 index 0000000..e185314 --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/.gitignore @@ -0,0 +1 @@ +elm-stuff \ No newline at end of file diff --git a/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/LICENSE b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/LICENSE new file mode 100644 index 0000000..81f65b3 --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017-present, Evan Czaplicki +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/README.md b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/README.md new file mode 100644 index 0000000..647a900 --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/README.md @@ -0,0 +1,7 @@ +# Parser Primitives + +**In 99.9999% of cases, you do not want this.** + +When creating a parser combinator library like [`elm-tools/parser`](https://github.com/elm-tools/parser), you want lower-level access to strings to get better performance. + +This package exposes these low-level functions so that `elm-tools/parser` does not have an unfair performance advantage. diff --git a/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/elm-package.json b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/elm-package.json new file mode 100644 index 0000000..c673a2d --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/elm-package.json @@ -0,0 +1,17 @@ +{ + "version": "1.0.0", + "summary": "Fast (but safe) primitives for creating parsing packages", + "repository": "https://github.com/elm-tools/parser-primitives.git", + "license": "BSD-3-Clause", + "source-directories": [ + "src" + ], + "exposed-modules": [ + "ParserPrimitives" + ], + "dependencies": { + "elm-lang/core": "5.0.0 <= v < 6.0.0" + }, + "native-modules": true, + "elm-version": "0.18.0 <= v < 0.19.0" +} diff --git a/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/src/Native/ParserPrimitives.js b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/src/Native/ParserPrimitives.js new file mode 100644 index 0000000..c7a6578 --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/src/Native/ParserPrimitives.js @@ -0,0 +1,130 @@ +var _elm_tools$parser_primitives$Native_ParserPrimitives = function() { + + +// STRINGS + +function isSubString(smallString, offset, row, col, bigString) +{ + var smallLength = smallString.length; + var bigLength = bigString.length - offset; + + if (bigLength < smallLength) + { + return tuple3(-1, row, col); + } + + for (var i = 0; i < smallLength; i++) + { + var char = smallString[i]; + + if (char !== bigString[offset + i]) + { + return tuple3(-1, row, col); + } + + // if it is a two word character + if ((bigString.charCodeAt(offset) & 0xF800) === 0xD800) + { + i++ + if (smallString[i] !== bigString[offset + i]) + { + return tuple3(-1, row, col); + } + col++; + continue; + } + + // if it is a newline + if (char === '\n') + { + row++; + col = 1; + continue; + } + + // if it is a one word character + col++ + } + + return tuple3(offset + smallLength, row, col); +} + +function tuple3(a, b, c) +{ + return { ctor: '_Tuple3', _0: a, _1: b, _2: c }; +} + + +// CHARS + +var mkChar = _elm_lang$core$Native_Utils.chr; + +function isSubChar(predicate, offset, string) +{ + if (offset >= string.length) + { + return -1; + } + + if ((string.charCodeAt(offset) & 0xF800) === 0xD800) + { + return predicate(mkChar(string.substr(offset, 2))) + ? offset + 2 + : -1; + } + + var char = string[offset]; + + return predicate(mkChar(char)) + ? ((char === '\n') ? -2 : (offset + 1)) + : -1; +} + + +// FIND STRING + +function findSubString(before, smallString, offset, row, col, bigString) +{ + var newOffset = bigString.indexOf(smallString, offset); + + if (newOffset === -1) + { + return tuple3(-1, row, col); + } + + var scanTarget = before ? newOffset : newOffset + smallString.length; + + while (offset < scanTarget) + { + var char = bigString[offset]; + + if (char === '\n') + { + offset++; + row++; + col = 1; + continue; + } + + if ((bigString.charCodeAt(offset) & 0xF800) === 0xD800) + { + offset += 2; + col++; + continue; + } + + offset++; + col++; + } + + return tuple3(offset, row, col); +} + + +return { + isSubString: F5(isSubString), + isSubChar: F3(isSubChar), + findSubString: F6(findSubString) +}; + +}(); diff --git a/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/src/ParserPrimitives.elm b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/src/ParserPrimitives.elm new file mode 100644 index 0000000..e93d647 --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser-primitives/1.0.0/src/ParserPrimitives.elm @@ -0,0 +1,109 @@ +module ParserPrimitives exposing + ( isSubString + , isSubChar + , findSubString + ) + +{-| Low-level functions for creating parser combinator libraries. + +@docs isSubString, isSubChar, findSubString +-} + +import Native.ParserPrimitives + + + +-- STRINGS + + +{-| When making a fast parser, you want to avoid allocation as much as +possible. That means you never want to mess with the source string, only +keep track of an offset into that string. + +You use `isSubString` like this: + + isSubString "let" offset row col "let x = 4 in x" + --==> ( newOffset, newRow, newCol ) + +You are looking for `"let"` at a given `offset`. On failure, the +`newOffset` is `-1`. On success, the `newOffset` is the new offset. With +our `"let"` example, it would be `offset + 3`. + +You also provide the current `row` and `col` which do not align with +`offset` in a clean way. For example, when you see a `\n` you are at +`row = row + 1` and `col = 1`. Furthermore, some UTF16 characters are +two words wide, so even if there are no newlines, `offset` and `col` +may not be equal. +-} +isSubString : String -> Int -> Int -> Int -> String -> (Int, Int, Int) +isSubString = + Native.ParserPrimitives.isSubString + + + +-- CHARACTERS + + +{-| Again, when parsing, you want to allocate as little as possible. +So this function lets you say: + + isSubChar isSpace offset "this is the source string" + --==> newOffset + +The `(Char -> Bool)` argument is called a predicate. +The `newOffset` value can be a few different things: + + - `-1` means that the predicate failed + - `-2` means the predicate succeeded with a `\n` + - otherwise you will get `offset + 1` or `offset + 2` + depending on whether the UTF16 character is one or two + words wide. + +It is better to use union types in general, but it is worth the +danger *within* parsing libraries to get the benefit *outside*. + +So you can write a `chomp` function like this: + + chomp : (Char -> Bool) -> Int -> Int -> Int -> String -> (Int, Int, Int) + chomp isGood offset row col source = + let + newOffset = + Prim.isSubChar isGood offset source + in + -- no match + if newOffset == -1 then + (offset, row, col) + + -- newline match + else if newOffset == -2 then + chomp isGood (offset + 1) (row + 1) 1 source + + -- normal match + else + chomp isGood newOffset row (col + 1) source + +Notice that `chomp` can be tail-call optimized, so this turns into a +`while` loop under the hood. +-} +isSubChar : (Char -> Bool) -> Int -> String -> Int +isSubChar = + Native.ParserPrimitives.isSubChar + + + +-- INDEX + + +{-| Find a substring after a given offset. + + findSubString before "42" offset row col "Is 42 the answer?" + --==> (newOffset, newRow, newCol) + +If `offset = 0` and `before = True` we would get `(3, 1, 4)` +If `offset = 0` and `before = False` we would get `(5, 1, 6)` + +If `offset = 7` we would get `(-1, 1, 18)` +-} +findSubString : Bool -> String -> Int -> Int -> Int -> String -> (Int, Int, Int) +findSubString = + Native.ParserPrimitives.findSubString diff --git a/finished/elm-stuff/packages/elm-tools/parser/2.0.1/.gitignore b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/.gitignore new file mode 100644 index 0000000..e185314 --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/.gitignore @@ -0,0 +1 @@ +elm-stuff \ No newline at end of file diff --git a/finished/elm-stuff/packages/elm-tools/parser/2.0.1/LICENSE b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/LICENSE new file mode 100644 index 0000000..81f65b3 --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017-present, Evan Czaplicki +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/finished/elm-stuff/packages/elm-tools/parser/2.0.1/README.md b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/README.md new file mode 100644 index 0000000..50289ed --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/README.md @@ -0,0 +1,170 @@ +# Parser + Nice Error Messages + +Goals: + + - Make writing parsers as simple and fun as possible. + - Produce excellent error messages. + - Go pretty fast. + +This is achieved with a couple concepts that I have not seen in any other parser libraries: [parser pipelines](#parser-pipelines), [tracking context](#tracking-context), and [delayed commits](#delayed-commits). + + +## Parser Pipelines + +To parse a 2D point like `( 3, 4 )`, you might create a `point` parser like this: + +```elm +import Parser exposing (Parser, (|.), (|=), succeed, symbol, float, ignore, zeroOrMore) + + +type alias Point = + { x : Float + , y : Float + } + + +point : Parser Point +point = + succeed Point + |. symbol "(" + |. spaces + |= float + |. spaces + |. symbol "," + |. spaces + |= float + |. spaces + |. symbol ")" + + +spaces : Parser () +spaces = + ignore zeroOrMore (\c -> c == ' ') +``` + +All the interesting stuff is happening in `point`. It uses two operators: + + - [`(|.)`][ignore] means “parse this, but **ignore** the result” + - [`(|=)`][keep] means “parse this, and **keep** the result” + +So the `Point` function only gets the result of the two `float` parsers. + +[ignore]: http://package.elm-lang.org/packages/elm-tools/parser/latest/Parser#|. +[keep]: http://package.elm-lang.org/packages/elm-tools/parser/latest/Parser#|= + +The theory is that `|=` introduces more “visual noise” than `|.`, making it pretty easy to pick out which lines in the pipeline are important. + +I recommend having one line per operator in your parser pipeline. If you need multiple lines for some reason, use a `let` or make a helper function. + + +## Tracking Context + +Most parsers tell you the row and column of the problem: + + Something went wrong at (4:17) + +That may be true, but it is not how humans think. It is how text editors think! It would be better to say: + + I found a problem with this list: + + [ 1, 23zm5, 3 ] + ^ + I wanted an integer, like 6 or 90219. + +Notice that the error messages says `this list`. That is context! That is the language my brain speaks, not rows and columns. + +This parser package lets you annotate context with the [`inContext`][inContext] function. You can let the parser know “I am trying to parse a `"list"` right now” so if an error happens anywhere in that context, you get the hand annotation! + +[inContext]: http://package.elm-lang.org/packages/elm-tools/parser/latest/Parser#inContext + +> **Note:** This technique is used by the parser in the Elm compiler to give more helpful error messages. + + +## Delayed Commits + +To make fast parsers with precise error messages, this package lets you control when a parser **commits** to a certain path. + +For example, you are trying to parse the following list: + +```elm +[ 1, 23zm5, 3 ] +``` + +Ideally, you want the error at the `z`, but the libraries I have seen make this difficult to achieve efficiently. You often end up with an error at `[` because “something went wrong”. + +**This package introduces [`delayedCommit`][delayedCommit] to resolve this.** + +Say we want to create `intList`, a parser for comma separated lists of integers like `[1, 2, 3]`. We would say something like this: + +[delayedCommit]: http://package.elm-lang.org/packages/elm-tools/parser/latest/Parser#delayedCommit + +```elm +import Parser exposing (..) + + +{-| We start by ignoring the opening square brace and some spaces. +We only really care about the numbers, so we parse an `int` and +then use `intListHelp` to start chomping other list entries. +-} +intList : Parser (List Int) +intList = + succeed identity + |. symbol "[" + |. spaces + |= andThen (\n -> intListHelp [n]) int + |. spaces + |. symbol "]" + + +{-| `intListHelp` checks if there is a `nextInt`. If so, it +continues trying to find more list items. If not, it gives +back the list of integers we have accumulated so far. +-} +intListHelp : List Int -> Parser (List Int) +intListHelp revInts = + oneOf + [ nextInt + |> andThen (\n -> intListHelp (n :: revInts)) + , succeed (List.reverse revInts) + ] +``` + +Now we get to the tricky part! How do we define `nextInt`? Here are two approaches, but only the second one actually works! + + +```elm +-- BAD +badNextInt : Parser Int +badNextInt = + succeed identity + |. spaces + |. symbol "," + |. spaces + |= int + +-- GOOD +nextInt : Parser Int +nextInt = + delayedCommit spaces <| + succeed identity + |. symbol "," + |. spaces + |= int +``` + +The `badNextInt` looks pretty normal, but it will not work. It commits as soon as the first `spaces` parser succeeds. It fails in the following situation: + +```elm +[ 1, 2, 3 ] + ^ +``` + +When we get to the closing `]` we have already successfully parsed some spaces. That means we are commited to `badNextInt` and need a comma. That fails, so the whole parse fails! + +With `nextInt`, the [`delayedCommit`][delayedCommit] function is saying to parse `spaces` but only commit if progress is made *after* that. So we are only commited to this parser if we see a comma. + +
+ +
+ +## [Comparison with Prior Work](https://github.com/elm-tools/parser/blob/master/comparison.md) diff --git a/finished/elm-stuff/packages/elm-tools/parser/2.0.1/comparison.md b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/comparison.md new file mode 100644 index 0000000..81ef545 --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/comparison.md @@ -0,0 +1,69 @@ +## Comparison with Prior Work + +I have not seen the [parser pipeline][1] or the [context stack][2] ideas in other libraries, but [delayed commits][3] relate to prior work. + +[1]: README.md#parser-pipelines +[2]: README.md#tracking-context +[3]: README.md#delayed-commits + +Most parser combinator libraries I have seen are based on Haskell’s Parsec library, which has primitives named `try` and `lookAhead`. I believe [`delayedCommitMap`][delayedCommitMap] is a better primitive for two reasons. + +[delayedCommitMap]: http://package.elm-lang.org/packages/elm-tools/parser/latest/Parser#delayedCommitMap + + +### Performance and Composition + +Say we want to create a precise error message for `length [1,,3]`. The naive approach with Haskell’s Parsec library produces very bad error messages: + +```haskell +spaceThenArg :: Parser Expr +spaceThenArg = + try (spaces >> term) +``` + +This means we get a precise error from `term`, but then throw it away and say something went wrong at the space before the `[`. Very confusing! To improve quality, we must write something like this: + +```haskell +spaceThenArg :: Parser Expr +spaceThenArg = + choice + [ do lookAhead (spaces >> char '[') + spaces + term + , try (spaces >> term) + ] +``` + +Notice that we parse `spaces` twice no matter what. + +Notice that we also had to hardcode `[` in the `lookAhead`. What if we update `term` to parse records that start with `{` as well? To get good commits on records, we must remember to update `lookAhead` to look for `oneOf "[{"`. Implementation details are leaking out of `term`! + +With `delayedCommit` in this Elm library, you can just say: + +```elm +spaceThenArg : Parser Expr +spaceThenArg = + delayedCommit spaces term +``` + +It does less work, and is more reliable as `term` evolves. I believe `delayedCommit` makes `lookAhead` pointless. + + +### Expressiveness + +You can define `try` in terms of [`delayedCommitMap`][delayedCommitMap] like this: + +```elm +try : Parser a -> Parser a +try parser = + delayedCommitMap always parser (succeed ()) +``` + +No expressiveness is lost! + +While it is possible to define `try`, I left it out of this package. In practice, `try` often leads to “bad commits” where your parser fails in a very specific way, but you then backtrack to a less specific error message. I considered naming it `allOrNothing` to better explain how it changes commit behavior, but ultimately, I thought it was best to encourage users to express their parsers with `delayedCommit` directly. + + +### Summary + +Compared to previous work, `delayedCommit` lets you produce precise error messages **more efficiently**. By thinking about “commit behavior” directly, you also end up with **cleaner composition** of parsers. And these benefits come **without any loss of expressiveness**. diff --git a/finished/elm-stuff/packages/elm-tools/parser/2.0.1/elm-package.json b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/elm-package.json new file mode 100644 index 0000000..7c82383 --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/elm-package.json @@ -0,0 +1,19 @@ +{ + "version": "2.0.1", + "summary": "a parsing library, focused on simplicity and great error messages", + "repository": "https://github.com/elm-tools/parser.git", + "license": "BSD-3-Clause", + "source-directories": [ + "src" + ], + "exposed-modules": [ + "Parser", + "Parser.LanguageKit", + "Parser.LowLevel" + ], + "dependencies": { + "elm-lang/core": "5.1.0 <= v < 6.0.0", + "elm-tools/parser-primitives": "1.0.0 <= v < 2.0.0" + }, + "elm-version": "0.18.0 <= v < 0.19.0" +} diff --git a/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser.elm b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser.elm new file mode 100644 index 0000000..163d26b --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser.elm @@ -0,0 +1,1116 @@ +module Parser exposing + ( Parser + , run + , int, float, symbol, keyword, end + , Count(..), zeroOrMore, oneOrMore, keep, ignore, repeat + , succeed, fail, map, oneOf, (|=), (|.), map2, lazy, andThen + , delayedCommit, delayedCommitMap + , source, sourceMap, ignoreUntil + , Error, Problem(..), Context, inContext + ) + +{-| + +# Parsers +@docs Parser, run + +# Numbers and Keywords +@docs int, float, symbol, keyword, end + +# Repeat Parsers +@docs Count, zeroOrMore, oneOrMore, keep, ignore, repeat + +# Combining Parsers +@docs succeed, fail, map, oneOf, (|=), (|.), map2, lazy, andThen + +# Delayed Commits +@docs delayedCommit, delayedCommitMap + +# Efficiency Tricks +@docs source, sourceMap, ignoreUntil + +# Errors +@docs Error, Problem, Context, inContext +-} + +import Char +import Parser.Internal as Internal exposing (Parser(..), Step(..)) +import ParserPrimitives as Prim + + + +-- PARSER + + +{-| A parser! If you have a `Parser Int`, it is a parser that turns +strings into integers. +-} +type alias Parser a = + Internal.Parser Context Problem a + + +type alias Step a = + Internal.Step Context Problem a + + +type alias State = + Internal.State Context + + +{-| Actually run a parser. + + run (keyword "true") "true" == Ok () + run (keyword "true") "True" == Err ... + run (keyword "true") "false" == Err ... +-} +run : Parser a -> String -> Result Error a +run (Parser parse) source = + let + initialState = + { source = source + , offset = 0 + , indent = 1 + , context = [] + , row = 1 + , col = 1 + } + in + case parse initialState of + Good a _ -> + Ok a + + Bad problem { row, col, context } -> + Err + { row = row + , col = col + , source = source + , problem = problem + , context = context + } + + +-- ERRORS + + +{-| Parse errors as data. You can format it however makes the most +sense for your application. Maybe that is all text, or maybe it is fancy +interactive HTML. Up to you! + +You get: + + - The `row` and `col` of the error. + - The full `source` provided to the [`run`](#run) function. + - The actual `problem` you ran into. + - A stack of `context` that describes where the error is *conceptually*. + +**Note:** `context` is a stack. That means [`inContext`](#inContext) +adds to the *front* of this list, not the back. So if you want the +[`Context`](#Context) closest to the error, you want the first element +of the `context` stack. +-} +type alias Error = + { row : Int + , col : Int + , source : String + , problem : Problem + , context : List Context + } + + +{-| The particular problem you ran into. + +The tricky one here is `BadRepeat`. That means that you are running +`zeroOrMore parser` where `parser` can succeed without consuming any +input. That means it will just loop forever, consuming no input until +the program crashes. +-} +type Problem + = BadOneOf (List Problem) + | BadInt + | BadFloat + | BadRepeat + | ExpectingEnd + | ExpectingSymbol String + | ExpectingKeyword String + | ExpectingVariable + | ExpectingClosing String + | Fail String + + +{-| Most parsers only let you know the row and column where the error +occurred. But what if you could *also* say “the error occured **while +parsing a list**” and let folks know what the *parser* thinks it is +doing?! + +The error messages would be a lot nicer! That is what Elm compiler does, +and it is what `Context` helps you do in this library! **See the +[`inContext`](#inContext) docs for a nice example!** + +About the actual fields: + + - `description` is set by [`inContext`](#inContext) + - `row` and `col` are where [`inContext`](#inContext) began + +Say you use `inContext` in your list parser. And say get an error trying +to parse `[ 1, 23zm5, 3 ]`. In addition to error information about `23zm5`, +you would have `Context` with the row and column of the starting `[` symbol. +-} +type alias Context = + { row : Int + , col : Int + , description : String + } + + + +-- PRIMITIVES + + +{-| A parser that succeeds without consuming any text. + + run (succeed 90210 ) "mississippi" == Ok 90210 + run (succeed 3.141 ) "mississippi" == Ok 3.141 + run (succeed () ) "mississippi" == Ok () + run (succeed Nothing) "mississippi" == Ok Nothing + +Seems weird, but it is often useful in combination with +[`oneOf`](#oneOf) or [`andThen`](#andThen). +-} +succeed : a -> Parser a +succeed a = + Parser <| \state -> Good a state + + +{-| A parser always fails. + + run (fail "bad list") "[1,2,3]" == Err .. + +Seems weird, but it is often useful in combination with +[`oneOf`](#oneOf) or [`andThen`](#andThen). +-} +fail : String -> Parser a +fail message = + Parser <| \state -> Bad (Fail message) state + + + +-- MAPPING + + +{-| Transform the result of a parser. Maybe you have a value that is +an integer or `null`: + + nullOrInt : Parser (Maybe Int) + nullOrInt = + oneOf + [ map Just int + , map (\_ -> Nothing) (keyword "null") + ] + + -- run nullOrInt "0" == Ok (Just 0) + -- run nullOrInt "13" == Ok (Just 13) + -- run nullOrInt "null" == Ok Nothing + -- run nullOrInt "zero" == Err ... + +-} +map : (a -> b) -> Parser a -> Parser b +map func (Parser parse) = + Parser <| \state1 -> + case parse state1 of + Good a state2 -> + Good (func a) state2 + + Bad x state2 -> + Bad x state2 + + +{-| **This function is not used much in practice.** It is nicer to use +the [parser pipeline][pp] operators [`(|.)`](#|.) and [`(|=)`](#|=) +instead. + +[pp]: https://github.com/elm-tools/parser/blob/master/README.md#parser-pipeline + +That said, this function can combine two parsers. Maybe you +want to parse some spaces followed by an integer: + + spacesThenInt : Parser Int + spacesThenInt = + map2 (\_ n -> n) spaces int + + spaces : Parser () + spaces = + ignore zeroOrMore (\char -> char == ' ') + +We can also use `map2` to define `(|.)` and `(|=)` like this: + + (|.) : Parser keep -> Parser ignore -> Parser keep + (|.) keepParser ignoreParser = + map2 (\keep _ -> keep) keepParser ignoreParser + + (|=) : Parser (a -> b) -> Parser a -> Parser b + (|=) funcParser argParser = + map2 (\func arg -> func arg) funcParser argParser +-} +map2 : (a -> b -> value) -> Parser a -> Parser b -> Parser value +map2 func (Parser parseA) (Parser parseB) = + Parser <| \state1 -> + case parseA state1 of + Bad x state2 -> + Bad x state2 + + Good a state2 -> + case parseB state2 of + Bad x state3 -> + Bad x state3 + + Good b state3 -> + Good (func a b) state3 + + +{-| **Keep** a value in a parser pipeline. + +Read about parser pipelines **[here][]**. They are really nice! + +[here]: https://github.com/elm-tools/parser/blob/master/README.md#parser-pipeline +-} +(|=) : Parser (a -> b) -> Parser a -> Parser b +(|=) parseFunc parseArg = + map2 apply parseFunc parseArg + + +apply : (a -> b) -> a -> b +apply f a = + f a + + +{-| **Ignore** a value in a parser pipeline. + +Read about parser pipelines **[here][]**. They are really nice! + +[here]: https://github.com/elm-tools/parser/blob/master/README.md#parser-pipeline +-} +(|.) : Parser keep -> Parser ignore -> Parser keep +(|.) keepParser ignoreParser = + map2 always keepParser ignoreParser + + +infixl 5 |. +infixl 5 |= + + + +-- AND THEN + + +{-| Run a parser *and then* run another parser! +-} +andThen : (a -> Parser b) -> Parser a -> Parser b +andThen callback (Parser parseA) = + Parser <| \state1 -> + case parseA state1 of + Bad x state2 -> + Bad x state2 + + Good a state2 -> + let + (Parser parseB) = + callback a + in + parseB state2 + + + +-- LAZY + + +{-| Helper to define recursive parsers. Say we want a parser for simple +boolean expressions: + + true + false + (true || false) + (true || (true || false)) + +Notice that a boolean expression might contain *other* boolean expressions. +That means we will want to define our parser in terms of itself: + + type Boolean + = MyTrue + | MyFalse + | MyOr Boolean Boolean + + boolean : Parser Boolean + boolean = + oneOf + [ succeed MyTrue + |. keyword "true" + , succeed MyFalse + |. keyword "false" + , succeed MyOr + |. symbol "(" + |. spaces + |= lazy (\_ -> boolean) + |. spaces + |. symbol "||" + |. spaces + |= lazy (\_ -> boolean) + |. spaces + |. symbol ")" + ] + + spaces : Parser () + spaces = + ignore zeroOrMore (\char -> char == ' ') + +**Notice that `boolean` uses `boolean` in its definition!** In Elm, you can +only define a value in terms of itself it is behind a function call. So +`lazy` helps us define these self-referential parsers. + +**Note:** In some cases, it may be more natural or efficient to use +`andThen` to hide a self-reference behind a function. +-} +lazy : (() -> Parser a) -> Parser a +lazy thunk = + Parser <| \state -> + let + (Parser parse) = + thunk () + in + parse state + + + +-- ONE OF + + +{-| Try a bunch of different parsers. If a parser does not commit, we +move on and try the next one. If a parser *does* commit, we give up on any +remaining parsers. + +The idea is: if you make progress and commit to a parser, you want to +get error messages from *that path*. If you bactrack and keep trying stuff +you will get a much less precise error. + +So say we are parsing “language terms” that include integers and lists +of integers: + + term : Parser Expr + term = + oneOf + [ listOf int + , int + ] + + listOf : Parser a -> Parser (List a) + listOf parser = + succeed identity + |. symbol "[" + |. spaces + ... + +When we get to `oneOf`, we first try the `listOf int` parser. If we see a +`[` we *commit* to that parser. That means if something goes wrong, we do +not backtrack. Instead the parse fails! If we do not see a `[` we move on +to the second option and just try the `int` parser. +-} +oneOf : List (Parser a) -> Parser a +oneOf parsers = + Parser <| \state -> oneOfHelp state [] parsers + + +oneOfHelp : State -> List Problem -> List (Parser a) -> Step a +oneOfHelp state problems parsers = + case parsers of + [] -> + Bad (BadOneOf (List.reverse problems)) state + + Parser parse :: remainingParsers -> + case parse state of + Good _ _ as step -> + step + + Bad problem { row, col } as step -> + if state.row == row && state.col == col then + oneOfHelp state (problem :: problems) remainingParsers + + else + step + + + +-- REPEAT + + +{-| Try to use the parser as many times as possible. Say we want to parse +`NaN` a bunch of times: + + batman : Parser Int + batman = + map List.length (repeat zeroOrMore (keyword "NaN")) + + -- run batman "whatever" == Ok 0 + -- run batman "" == Ok 0 + -- run batman "NaN" == Ok 1 + -- run batman "NaNNaN" == Ok 2 + -- run batman "NaNNaNNaN" == Ok 3 + -- run batman "NaNNaN batman!" == Ok 2 + +**Note:** If you are trying to parse things like `[1,2,3]` or `{ x = 3 }` +check out the [`list`](Parser-LanguageKit#list) and +[`record`](Parser-LanguageKit#record) functions in the +[`Parser.LanguageKit`](Parser-LanguageKit) module. +-} +repeat : Count -> Parser a -> Parser (List a) +repeat count (Parser parse) = + case count of + Exactly n -> + Parser <| \state -> + repeatExactly n parse [] state + + AtLeast n -> + Parser <| \state -> + repeatAtLeast n parse [] state + + +repeatExactly : Int -> (State -> Step a) -> List a -> State -> Step (List a) +repeatExactly n parse revList state1 = + if n <= 0 then + Good (List.reverse revList) state1 + + else + case parse state1 of + Good a state2 -> + if state1.row == state2.row && state1.col == state2.col then + Bad BadRepeat state2 + else + repeatExactly (n - 1) parse (a :: revList) state2 + + Bad x state2 -> + Bad x state2 + + +repeatAtLeast : Int -> (State -> Step a) -> List a -> State -> Step (List a) +repeatAtLeast n parse revList state1 = + case parse state1 of + Good a state2 -> + if state1.row == state2.row && state1.col == state2.col then + Bad BadRepeat state2 + else + repeatAtLeast (n - 1) parse (a :: revList) state2 + + Bad x state2 -> + if state1.row == state2.row && state1.col == state2.col && n <= 0 then + Good (List.reverse revList) state1 + + else + Bad x state2 + + + +-- DELAYED COMMIT + + +{-| Only commit if `Parser a` succeeds and `Parser value` makes some progress. + +This is very important for generating high quality error messages! Read more +about this [here][1] and [here][2]. + +[1]: https://github.com/elm-tools/parser/blob/master/README.md#delayed-commits +[2]: https://github.com/elm-tools/parser/blob/master/comparison.md +-} +delayedCommit : Parser a -> Parser value -> Parser value +delayedCommit filler realStuff = + delayedCommitMap (\_ v -> v) filler realStuff + + +{-| Like [`delayedCommit`](#delayedCommit), but lets you extract values from +both parsers. Read more about it [here][1] and [here][2]. + +[1]: https://github.com/elm-tools/parser/blob/master/README.md#delayed-commits +[2]: https://github.com/elm-tools/parser/blob/master/comparison.md +-} +delayedCommitMap : (a -> b -> value) -> Parser a -> Parser b -> Parser value +delayedCommitMap func (Parser parseA) (Parser parseB) = + Parser <| \state1 -> + case parseA state1 of + Bad x _ -> + Bad x state1 + + Good a state2 -> + case parseB state2 of + Good b state3 -> + Good (func a b) state3 + + Bad x state3 -> + if state2.row == state3.row && state2.col == state3.col then + Bad x state1 + else + Bad x state3 + + + +-- SYMBOLS and KEYWORDS + + +{-| Parse symbols like `,`, `(`, and `&&`. + + run (symbol "[") "[" == Ok () + run (symbol "[") "4" == Err ... (ExpectingSymbol "[") ... +-} +symbol : String -> Parser () +symbol str = + token ExpectingSymbol str + + +{-| Parse keywords like `let`, `case`, and `type`. + + run (keyword "let") "let" == Ok () + run (keyword "let") "var" == Err ... (ExpectingKeyword "let") ... +-} +keyword : String -> Parser () +keyword str = + token ExpectingKeyword str + + +token : (String -> Problem) -> String -> Parser () +token makeProblem str = + Parser <| \({ source, offset, indent, context, row, col } as state) -> + let + (newOffset, newRow, newCol) = + Prim.isSubString str offset row col source + in + if newOffset == -1 then + Bad (makeProblem str) state + + else + Good () + { source = source + , offset = newOffset + , indent = indent + , context = context + , row = newRow + , col = newCol + } + + +-- INT + + +{-| Parse integers. It accepts decimal and hexidecimal formats. + + -- decimal + run int "1234" == Ok 1234 + run int "1.34" == Err ... + run int "1e31" == Err ... + run int "123a" == Err ... + run int "0123" == Err ... + + -- hexidecimal + run int "0x001A" == Ok 26 + run int "0x001a" == Ok 26 + run int "0xBEEF" == Ok 48879 + run int "0x12.0" == Err ... + run int "0x12an" == Err ... + +**Note:** If you want a parser for both `Int` and `Float` literals, +check out [`Parser.LanguageKit.number`](Parser-LanguageKit#number). +It does not backtrack, so it should be faster and give better error +messages than using `oneOf` and combining `int` and `float` yourself. + +**Note:** If you want to enable octal or binary `Int` literals, +check out [`Parser.LanguageKit.int`](Parser-LanguageKit#int). +-} +int : Parser Int +int = + Parser <| \{ source, offset, indent, context, row, col } -> + case intHelp offset (Prim.isSubChar isZero offset source) source of + Err badOffset -> + Bad BadInt + { source = source + , offset = badOffset + , indent = indent + , context = context + , row = row + , col = col + (badOffset - offset) + } + + Ok goodOffset -> + case String.toInt (String.slice offset goodOffset source) of + Err _ -> + Debug.crash badIntMsg + + Ok n -> + Good n + { source = source + , offset = goodOffset + , indent = indent + , context = context + , row = row + , col = col + (goodOffset - offset) + } + + +intHelp : Int -> Int -> String -> Result Int Int +intHelp offset zeroOffset source = + if zeroOffset == -1 then + Internal.chompDigits Char.isDigit offset source + + else if Prim.isSubChar isX zeroOffset source /= -1 then + Internal.chompDigits Char.isHexDigit (offset + 2) source + +-- else if Prim.isSubChar isO zeroOffset source /= -1 then +-- Internal.chompDigits Char.isOctDigit (offset + 2) source + + else if Prim.isSubChar Internal.isBadIntEnd zeroOffset source == -1 then + Ok zeroOffset + + else + Err zeroOffset + + +isZero : Char -> Bool +isZero char = + char == '0' + + +isO : Char -> Bool +isO char = + char == 'o' + + +isX : Char -> Bool +isX char = + char == 'x' + + +badIntMsg : String +badIntMsg = + """The `Parser.int` parser seems to have a bug. +Please report an SSCCE to .""" + + + +-- FLOAT + + +{-| Parse floats. + + run float "123" == Ok 123 + run float "3.1415" == Ok 3.1415 + run float "0.1234" == Ok 0.1234 + run float ".1234" == Ok 0.1234 + run float "1e-42" == Ok 1e-42 + run float "6.022e23" == Ok 6.022e23 + run float "6.022E23" == Ok 6.022e23 + run float "6.022e+23" == Ok 6.022e23 + run float "6.022e" == Err .. + run float "6.022n" == Err .. + run float "6.022.31" == Err .. + +**Note:** If you want a parser for both `Int` and `Float` literals, +check out [`Parser.LanguageKit.number`](Parser-LanguageKit#number). +It does not backtrack, so it should be faster and give better error +messages than using `oneOf` and combining `int` and `float` yourself. + +**Note:** If you want to disable literals like `.123` like Elm, +check out [`Parser.LanguageKit.float`](Parser-LanguageKit#float). +-} +float : Parser Float +float = + Parser <| \{ source, offset, indent, context, row, col } -> + case floatHelp offset (Prim.isSubChar isZero offset source) source of + Err badOffset -> + Bad BadFloat + { source = source + , offset = badOffset + , indent = indent + , context = context + , row = row + , col = col + (badOffset - offset) + } + + Ok goodOffset -> + case String.toFloat (String.slice offset goodOffset source) of + Err _ -> + Debug.crash badFloatMsg + + Ok n -> + Good n + { source = source + , offset = goodOffset + , indent = indent + , context = context + , row = row + , col = col + (goodOffset - offset) + } + + +floatHelp : Int -> Int -> String -> Result Int Int +floatHelp offset zeroOffset source = + if zeroOffset >= 0 then + Internal.chompDotAndExp zeroOffset source + + else + let + dotOffset = + Internal.chomp Char.isDigit offset source + + result = + Internal.chompDotAndExp dotOffset source + in + case result of + Err _ -> + result + + Ok n -> + if n == offset then Err n else result + + +badFloatMsg : String +badFloatMsg = + """The `Parser.float` parser seems to have a bug. +Please report an SSCCE to .""" + + + +-- END + + +{-| Check if you have reached the end of the string you are parsing. + + justAnInt : Parser Int + justAnInt = + succeed identity + |= int + |. end + + -- run justAnInt "90210" == Ok 90210 + -- run justAnInt "1 + 2" == Err ... + -- run int "1 + 2" == Ok 1 + +Parsers can succeed without parsing the whole string. Ending your parser +with `end` guarantees that you have successfully parsed the whole string. +-} +end : Parser () +end = + Parser <| \state -> + if String.length state.source == state.offset then + Good () state + + else + Bad ExpectingEnd state + + + +-- SOURCE + + +{-| Run a parser, but return the underlying source code that actually +got parsed. + + -- run (source (ignore oneOrMore Char.isLower)) "abc" == Ok "abc" + -- keep count isOk = source (ignore count isOk) + +This becomes a useful optimization when you need to [`keep`](#keep) +something very specific. For example, say we want to parse capitalized +words: + + import Char + + variable : Parser String + variable = + succeed (++) + |= keep (Exactly 1) Char.isUpper + |= keep zeroOrMore Char.isLower + +In this case, each `keep` allocates a string. Then we use `(++)` to create the +final string. That means *three* strings are allocated. + +In contrast, using `source` with `ignore` lets you grab the final string +directly. It tracks where the parser starts and ends, so it can use +`String.slice` to grab that part directly. + + variable : Parser String + variable = + source <| + ignore (Exactly 1) Char.isUpper + |. ignore zeroOrMore Char.isLower + +This version only allocates *one* string. +-} +source : Parser a -> Parser String +source parser = + sourceMap always parser + + +{-| Like `source`, but it allows you to combine the source string +with the value that is produced by the parser. So maybe you want +a float, but you also want to know exactly how it looked. + + number : Parser (String, Float) + number = + sourceMap (,) float + + -- run number "100" == Ok ("100", 100) + -- run number "1e2" == Ok ("1e2", 100) +-} +sourceMap : (String -> a -> b) -> Parser a -> Parser b +sourceMap func (Parser parse) = + Parser <| \({source, offset} as state1) -> + case parse state1 of + Bad x state2 -> + Bad x state2 + + Good a state2 -> + let + subString = + String.slice offset state2.offset source + in + Good (func subString a) state2 + + + +-- REPEAT + + +{-| How many characters to [`keep`](#keep) or [`ignore`](#ignore). +-} +type Count = AtLeast Int | Exactly Int + + +{-| A simple alias for `AtLeast 0` so your code reads nicer: + + import Char + + spaces : Parser String + spaces = + keep zeroOrMore (\c -> c == ' ') + + -- same as: keep (AtLeast 0) (\c -> c == ' ') +-} +zeroOrMore : Count +zeroOrMore = + AtLeast 0 + + +{-| A simple alias for `AtLeast 1` so your code reads nicer: + + import Char + + lows : Parser String + lows = + keep oneOrMore Char.isLower + + -- same as: keep (AtLeast 1) Char.isLower +-} +oneOrMore : Count +oneOrMore = + AtLeast 1 + + +{-| Keep some characters. If you want a capital letter followed by +zero or more lower case letters, you could say: + + import Char + + capitalized : Parser String + capitalized = + succeed (++) + |= keep (Exactly 1) Char.isUpper + |= keep zeroOrMore Char.isLower + + -- good: Cat, Tom, Sally + -- bad: cat, tom, TOM, tOm + +**Note:** Check out [`source`](#source) for a more efficient +way to grab the underlying source of a complex parser. +-} +keep : Count -> (Char -> Bool) -> Parser String +keep count predicate = + source (ignore count predicate) + + +{-| Ignore some characters. If you want to ignore one or more +spaces, you might say: + + spaces : Parser () + spaces = + ignore oneOrMore (\c -> c == ' ') + +-} +ignore : Count -> (Char -> Bool) -> Parser () +ignore count predicate = + case count of + Exactly n -> + Parser <| \{ source, offset, indent, context, row, col } -> + ignoreExactly n predicate source offset indent context row col + + AtLeast n -> + Parser <| \{ source, offset, indent, context, row, col } -> + ignoreAtLeast n predicate source offset indent context row col + + +ignoreExactly : Int -> (Char -> Bool) -> String -> Int -> Int -> List Context -> Int -> Int -> Step () +ignoreExactly n predicate source offset indent context row col = + if n <= 0 then + Good () + { source = source + , offset = offset + , indent = indent + , context = context + , row = row + , col = col + } + + else + let + newOffset = + Prim.isSubChar predicate offset source + in + if newOffset == -1 then + Bad BadRepeat + { source = source + , offset = offset + , indent = indent + , context = context + , row = row + , col = col + } + + else if newOffset == -2 then + ignoreExactly (n - 1) predicate source (offset + 1) indent context (row + 1) 1 + + else + ignoreExactly (n - 1) predicate source newOffset indent context row (col + 1) + + +ignoreAtLeast : Int -> (Char -> Bool) -> String -> Int -> Int -> List Context -> Int -> Int -> Step () +ignoreAtLeast n predicate source offset indent context row col = + let + newOffset = + Prim.isSubChar predicate offset source + in + -- no match + if newOffset == -1 then + let + state = + { source = source + , offset = offset + , indent = indent + , context = context + , row = row + , col = col + } + in + if n <= 0 then Good () state else Bad BadRepeat state + + -- matched a newline + else if newOffset == -2 then + ignoreAtLeast (n - 1) predicate source (offset + 1) indent context (row + 1) 1 + + -- normal match + else + ignoreAtLeast (n - 1) predicate source newOffset indent context row (col + 1) + + + +-- IGNORE UNTIL + + +{-| Ignore characters until *after* the given string. +So maybe we want to parse Elm-style single-line comments: + + elmComment : Parser () + elmComment = + symbol "--" + |. ignoreUntil "\n" + +Or maybe you want to parse JS-style multi-line comments: + + jsComment : Parser () + jsComment = + symbol "/*" + |. ignoreUntil "*/" + +**Note:** You must take more care when parsing Elm-style multi-line +comments. Elm can recognize nested comments, but the `jsComment` parser +cannot. See [`Parser.LanguageKit.whitespace`](Parser-LanguageKit#whitespace) +for help with this. +-} +ignoreUntil : String -> Parser () +ignoreUntil str = + Parser <| \({ source, offset, indent, context, row, col } as state) -> + let + (newOffset, newRow, newCol) = + Prim.findSubString False str offset row col source + in + if newOffset == -1 then + Bad (ExpectingClosing str) state + + else + Good () + { source = source + , offset = newOffset + , indent = indent + , context = context + , row = newRow + , col = newCol + } + + + +-- CONTEXT + + +{-| Specify what you are parsing right now. So if you have a parser +for lists like `[ 1, 2, 3 ]` you could say: + + list : Parser (List Int) + list = + inContext "list" <| + succeed identity + |. symbol "[" + |. spaces + |= commaSep int + |. spaces + |. symbol "]" + + -- spaces : Parser () + -- commaSep : Parser a -> Parser (List a) + +Now you get that extra context information if there is a parse error anywhere +in the list. For example, if you have `[ 1, 23zm5, 3 ]` you could generate an +error message like this: + + I ran into a problem while parsing this list: + + [ 1, 23zm5, 3 ] + ^ + Looking for a valid integer, like 6 or 90210. + +Notice that the error message knows you are parsing a list right now! +-} +inContext : String -> Parser a -> Parser a +inContext ctx (Parser parse) = + Parser <| \({ context, row, col } as initialState) -> + let + state1 = + changeContext (Context row col ctx :: context) initialState + in + case parse state1 of + Good a state2 -> + Good a (changeContext context state2) + + Bad _ _ as step -> + step + + +changeContext : List Context -> State -> State +changeContext newContext { source, offset, indent, row, col } = + { source = source + , offset = offset + , indent = indent + , context = newContext + , row = row + , col = col + } diff --git a/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/Internal.elm b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/Internal.elm new file mode 100644 index 0000000..cd07186 --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/Internal.elm @@ -0,0 +1,149 @@ +module Parser.Internal exposing + ( Parser(..) + , Step(..) + , State + , chomp + , chompDigits + , chompDotAndExp + , isBadIntEnd + ) + + +import Char +import ParserPrimitives as Prim + + + +-- PARSERS + + +type Parser ctx x a = + Parser (State ctx -> Step ctx x a) + + +type Step ctx x a + = Good a (State ctx) + | Bad x (State ctx) + + +type alias State ctx = + { source : String + , offset : Int + , indent : Int + , context : List ctx + , row : Int + , col : Int + } + + + +-- CHOMPERS + + +chomp : (Char -> Bool) -> Int -> String -> Int +chomp isGood offset source = + let + newOffset = + Prim.isSubChar isGood offset source + in + if newOffset < 0 then + offset + + else + chomp isGood newOffset source + + + +-- CHOMP DIGITS + + +chompDigits : (Char -> Bool) -> Int -> String -> Result Int Int +chompDigits isValidDigit offset source = + let + newOffset = + chomp isValidDigit offset source + in + -- no digits + if newOffset == offset then + Err newOffset + + -- ends with non-digit characters + else if Prim.isSubChar isBadIntEnd newOffset source /= -1 then + Err newOffset + + -- all valid digits! + else + Ok newOffset + + +isBadIntEnd : Char -> Bool +isBadIntEnd char = + Char.isDigit char + || Char.isUpper char + || Char.isLower char + || char == '.' + + + +-- CHOMP FLOAT STUFF + + +chompDotAndExp : Int -> String -> Result Int Int +chompDotAndExp offset source = + let + dotOffset = + Prim.isSubChar isDot offset source + in + if dotOffset == -1 then + chompExp offset source + + else + chompExp (chomp Char.isDigit dotOffset source) source + + +isDot : Char -> Bool +isDot char = + char == '.' + + +chompExp : Int -> String -> Result Int Int +chompExp offset source = + let + eOffset = + Prim.isSubChar isE offset source + in + if eOffset == -1 then + Ok offset + + else + let + opOffset = + Prim.isSubChar isPlusOrMinus eOffset source + + expOffset = + if opOffset == -1 then eOffset else opOffset + in + if Prim.isSubChar isZero expOffset source /= -1 then + Err expOffset + + else if Prim.isSubChar Char.isDigit expOffset source == -1 then + Err expOffset + + else + chompDigits Char.isDigit expOffset source + + +isE : Char -> Bool +isE char = + char == 'e' || char == 'E' + + +isZero : Char -> Bool +isZero char = + char == '0' + + +isPlusOrMinus : Char -> Bool +isPlusOrMinus char = + char == '+' || char == '-' + diff --git a/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/LanguageKit.elm b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/LanguageKit.elm new file mode 100644 index 0000000..4b6e4ea --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/LanguageKit.elm @@ -0,0 +1,509 @@ +module Parser.LanguageKit exposing + ( variable + , list, record, tuple, sequence, Trailing(..) + , whitespace, LineComment(..), MultiComment(..) + ) + + +{-| + +# Variables +@docs variable + +# Lists, records, and that sort of thing +@docs list, record, tuple, sequence, Trailing + +# Whitespace +@docs whitespace, LineComment, MultiComment + +-} + + +import Set exposing (Set) +import Parser exposing (..) +import Parser.Internal as I exposing (Step(..), State) +import ParserPrimitives as Prim + + + +-- VARIABLES + + +{-| Create a parser for variables. It takes two `Char` checkers. The +first one is for the first character. The second one is for all the +other characters. + +In Elm, we distinguish between upper and lower case variables, so we +can do something like this: + + import Char + import Parser exposing (..) + import Parser.LanguageKit exposing (variable) + import Set + + lowVar : Parser String + lowVar = + variable Char.isLower isVarChar keywords + + capVar : Parser String + capVar = + variable Char.isUpper isVarChar keywords + + isVarChar : Char -> Bool + isVarChar char = + Char.isLower char + || Char.isUpper char + || Char.isDigit char + || char == '_' + + keywords : Set.Set String + keywords = + Set.fromList [ "let", "in", "case", "of" ] +-} +variable : (Char -> Bool) -> (Char -> Bool) -> Set String -> Parser String +variable isFirst isOther keywords = + I.Parser <| \({ source, offset, indent, context, row, col } as state1) -> + let + firstOffset = + Prim.isSubChar isFirst offset source + in + if firstOffset == -1 then + Bad ExpectingVariable state1 + + else + let + state2 = + if firstOffset == -2 then + varHelp isOther (offset + 1) (row + 1) 1 source indent context + else + varHelp isOther firstOffset row (col + 1) source indent context + + name = + String.slice offset state2.offset source + in + if Set.member name keywords then + Bad ExpectingVariable state1 + + else + Good name state2 + + +varHelp : (Char -> Bool) -> Int -> Int -> Int -> String -> Int -> List ctx -> State ctx +varHelp isGood offset row col source indent context = + let + newOffset = + Prim.isSubChar isGood offset source + in + if newOffset == -1 then + { source = source + , offset = offset + , indent = indent + , context = context + , row = row + , col = col + } + + else if newOffset == -2 then + varHelp isGood (offset + 1) (row + 1) 1 source indent context + + else + varHelp isGood newOffset row (col + 1) source indent context + + + +-- SEQUENCES + + +{-| Parse a comma-separated list like `[ 1, 2, 3 ]`. You provide +a parser for the spaces and for the list items. So if you want +to parse a list of integers, you would say: + + import Parser exposing (Parser) + import Parser.LanguageKit as Parser + + intList : Parser (List Int) + intList = + Parser.list spaces Parser.int + + spaces : Parser () + spaces = + Parser.ignore zeroOrMore (\char -> char == ' ') + + -- run intList "[]" == Ok [] + -- run intList "[ ]" == Ok [] + -- run intList "[1,2,3]" == Ok [1,2,3] + -- run intList "[ 1, 2, 3 ]" == Ok [1,2,3] + -- run intList "[ 1 , 2 , 3 ]" == Ok [1,2,3] + -- run intList "[ 1, 2, 3, ]" == Err ... + -- run intList "[, 1, 2, 3 ]" == Err ... + +**Note:** If you want trailing commas, check out the +[`sequence`](#sequence) function. +-} +list : Parser () -> Parser a -> Parser (List a) +list spaces item = + sequence + { start = "[" + , separator = "," + , end = "]" + , spaces = spaces + , item = item + , trailing = Forbidden + } + + +{-| Help parse records like `{ a = 2, b = 2 }`. You provide +a parser for the spaces and for the list items, you might say: + + import Parser exposing ( Parser, (|.), (|=), zeroOrMore ) + import Parser.LanguageKit as Parser + + record : Parser (List (String, Int)) + record = + Parser.record spaces field + + field : Parser (String, Int) + field = + Parser.succeed (,) + |= lowVar + |. spaces + |. Parser.symbol "=" + |. spaces + |= int + + spaces : Parser () + spaces = + Parser.ignore zeroOrMore (\char -> char == ' ') + + -- run record "{}" == Ok [] + -- run record "{ }" == Ok [] + -- run record "{ x = 3 }" == Ok [ ("x",3) ] + -- run record "{ x = 3, }" == Err ... + -- run record "{ x = 3, y = 4 }" == Ok [ ("x",3), ("y",4) ] + -- run record "{ x = 3, y = }" == Err ... + +**Note:** If you want trailing commas, check out the +[`sequence`](#sequence) function. +-} +record : Parser () -> Parser a -> Parser (List a) +record spaces item = + sequence + { start = "{" + , separator = "," + , end = "}" + , spaces = spaces + , item = item + , trailing = Forbidden + } + + +{-| Help parse tuples like `(3, 4)`. Works just like [`list`](#list) +and [`record`](#record). And if you need something custom, check out +the [`sequence`](#sequence) function. +-} +tuple : Parser () -> Parser a -> Parser (List a) +tuple spaces item = + sequence + { start = "(" + , separator = "," + , end = ")" + , spaces = spaces + , item = item + , trailing = Forbidden + } + + +{-| Handle things *like* lists and records, but you can customize the +details however you need. Say you want to parse C-style code blocks: + + import Parser exposing (Parser) + import Parser.LanguageKit as Parser exposing (Trailing(..)) + + block : Parser (List Stmt) + block = + Parser.sequence + { start = "{" + , separator = ";" + , end = "}" + , spaces = spaces + , item = statement + , trailing = Mandatory -- demand a trailing semi-colon + } + + -- spaces : Parser () + -- statement : Parser Stmt + +**Note:** If you need something more custom, do not be afraid to check +out the implementation and customize it for your case. It is better to +get nice error messages with a lower-level implementation than to try +to hack high-level parsers to do things they are not made for. +-} +sequence + : { start : String + , separator : String + , end : String + , spaces : Parser () + , item : Parser a + , trailing : Trailing + } + -> Parser (List a) +sequence { start, end, spaces, item, separator, trailing } = + symbol start + |- spaces + |- sequenceEnd end spaces item separator trailing + + +{-| What’s the deal with trailing commas? Are they `Forbidden`? +Are they `Optional`? Are they `Mandatory`? Welcome to [shapes +club](http://poorlydrawnlines.com/comic/shapes-club/)! +-} +type Trailing = Forbidden | Optional | Mandatory + + +ignore : Parser ignore -> Parser keep -> Parser keep +ignore ignoreParser keepParser = + map2 revAlways ignoreParser keepParser + + +(|-) : Parser ignore -> Parser keep -> Parser keep +(|-) = + ignore + + +revAlways : ignore -> keep -> keep +revAlways _ keep = + keep + + +sequenceEnd : String -> Parser () -> Parser a -> String -> Trailing -> Parser (List a) +sequenceEnd end spaces parseItem sep trailing = + let + chompRest item = + case trailing of + Forbidden -> + sequenceEndForbidden end spaces parseItem sep [item] + + Optional -> + sequenceEndOptional end spaces parseItem sep [item] + + Mandatory -> + spaces + |- symbol sep + |- spaces + |- sequenceEndMandatory end spaces parseItem sep [item] + in + oneOf + [ parseItem + |> andThen chompRest + , symbol end + |- succeed [] + ] + + +sequenceEndForbidden : String -> Parser () -> Parser a -> String -> List a -> Parser (List a) +sequenceEndForbidden end spaces parseItem sep revItems = + let + chompRest item = + sequenceEndForbidden end spaces parseItem sep (item :: revItems) + in + ignore spaces <| + oneOf + [ symbol sep + |- spaces + |- andThen chompRest parseItem + , symbol end + |- succeed (List.reverse revItems) + ] + + +sequenceEndOptional : String -> Parser () -> Parser a -> String -> List a -> Parser (List a) +sequenceEndOptional end spaces parseItem sep revItems = + let + parseEnd = + andThen (\_ -> succeed (List.reverse revItems)) (symbol end) + + chompRest item = + sequenceEndOptional end spaces parseItem sep (item :: revItems) + in + ignore spaces <| + oneOf + [ symbol sep + |- spaces + |- oneOf [ andThen chompRest parseItem, parseEnd ] + , parseEnd + ] + + +sequenceEndMandatory : String -> Parser () -> Parser a -> String -> List a -> Parser (List a) +sequenceEndMandatory end spaces parseItem sep revItems = + let + chompRest item = + sequenceEndMandatory end spaces parseItem sep (item :: revItems) + in + oneOf + [ andThen chompRest <| + parseItem + |. spaces + |. symbol sep + |. spaces + , symbol end + |- succeed (List.reverse revItems) + ] + + + +-- WHITESPACE + + +{-| Create a custom whitespace parser. It will always chomp the +`' '`, `'\r'`, and `'\n'` characters, but you can customize some +other things. Here are some examples: + + elm : Parser () + elm = + whitespace + { allowTabs = False + , lineComment = LineComment "--" + , multiComment = NestableComment "{-" "-}" + } + + js : Parser () + js = + whitespace + { allowTabs = True + , lineComment = LineComment "//" + , multiComment = UnnestableComment "/*" "*/" + } + +If you need further customization, please open an issue describing your +scenario or check out the source code and write it yourself. This is all +built using stuff from the root `Parser` module. +-} +whitespace + : { allowTabs : Bool + , lineComment : LineComment + , multiComment : MultiComment + } + -> Parser () +whitespace { allowTabs, lineComment, multiComment } = + let + tabParser = + if allowTabs then + [ Parser.ignore zeroOrMore isTab ] + else + [] + + lineParser = + case lineComment of + NoLineComment -> + [] + + LineComment start -> + [ symbol start + |. ignoreUntil "\n" + ] + + multiParser = + case multiComment of + NoMultiComment -> + [] + + UnnestableComment start end -> + [ symbol start + |. ignoreUntil end + ] + + NestableComment start end -> + [ nestableComment start end + ] + in + whitespaceHelp <| + oneOf (tabParser ++ lineParser ++ multiParser) + + +chompSpaces : Parser () +chompSpaces = + Parser.ignore zeroOrMore isSpace + + +isSpace : Char -> Bool +isSpace char = + char == ' ' || char == '\n' || char == '\r' + + +isTab : Char -> Bool +isTab char = + char == '\t' + + +whitespaceHelp : Parser a -> Parser () +whitespaceHelp parser = + ignore chompSpaces <| + oneOf [ andThen (\_ -> whitespaceHelp parser) parser, succeed () ] + + +{-| Are line comments allowed? If so, what symbol do they start with? + + LineComment "--" -- Elm + LineComment "//" -- JS + LineComment "#" -- Python + NoLineComment -- OCaml +-} +type LineComment = NoLineComment | LineComment String + + +{-| Are multi-line comments allowed? If so, what symbols do they start +and end with? + + NestableComment "{-" "-}" -- Elm + UnnestableComment "/*" "*/" -- JS + NoMultiComment -- Python + +In Elm, you can nest multi-line comments. In C-like languages, like JS, +this is not allowed. As soon as you see a `*/` the comment is over no +matter what. +-} +type MultiComment + = NoMultiComment + | NestableComment String String + | UnnestableComment String String + + +nestableComment : String -> String -> Parser () +nestableComment start end = + case (String.uncons start, String.uncons end) of + (Nothing, _) -> + fail "Trying to parse a multi-line comment, but the start token cannot be the empty string!" + + (_, Nothing) -> + fail "Trying to parse a multi-line comment, but the end token cannot be the empty string!" + + ( Just (startChar, _), Just (endChar, _) ) -> + let + isNotRelevant char = + char /= startChar && char /= endChar + in + symbol start + |. nestableCommentHelp isNotRelevant start end 1 + + +nestableCommentHelp : (Char -> Bool) -> String -> String -> Int -> Parser () +nestableCommentHelp isNotRelevant start end nestLevel = + lazy <| \_ -> + ignore (Parser.ignore zeroOrMore isNotRelevant) <| + oneOf + [ ignore (symbol end) <| + if nestLevel == 1 then + succeed () + else + nestableCommentHelp isNotRelevant start end (nestLevel - 1) + , ignore (symbol start) <| + nestableCommentHelp isNotRelevant start end (nestLevel + 1) + , ignore (Parser.ignore (Exactly 1) isChar) <| + nestableCommentHelp isNotRelevant start end nestLevel + ] + + +isChar : Char -> Bool +isChar char = + True diff --git a/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/LowLevel.elm b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/LowLevel.elm new file mode 100644 index 0000000..653c347 --- /dev/null +++ b/finished/elm-stuff/packages/elm-tools/parser/2.0.1/src/Parser/LowLevel.elm @@ -0,0 +1,118 @@ +module Parser.LowLevel exposing + ( getIndentLevel + , withIndentLevel + + , getPosition + , getRow + , getCol + + , getOffset + , getSource + ) + +{-| You are unlikely to need any of this under normal circumstances. + +# Indentation +@docs getIndentLevel, withIndentLevel + +# Row, Column, Offset, and Source +@docs getPosition, getRow, getCol, getOffset, getSource + +-} + +import Parser exposing (Parser) +import Parser.Internal as I exposing (State) + + + +-- INDENTATION + + +{-| This parser tracks “indentation level” so you can parse indentation +sensitive languages. Indentation levels correspond to column numbers, so +it starts at 1. +-} +getIndentLevel : Parser Int +getIndentLevel = + I.Parser <| \state -> I.Good state.indent state + + +{-| Run a parser with a given indentation level. So you will likely +use `getCol` to get the current column, `andThen` give that to +`withIndentLevel`. +-} +withIndentLevel : Int -> Parser a -> Parser a +withIndentLevel newIndent (I.Parser parse) = + I.Parser <| \state1 -> + case parse (changeIndent newIndent state1) of + I.Good a state2 -> + I.Good a (changeIndent state1.indent state2) + + I.Bad x state2 -> + I.Bad x (changeIndent state1.indent state2) + + +changeIndent : Int -> State ctx -> State ctx +changeIndent newIndent { source, offset, context, row, col } = + { source = source + , offset = offset + , indent = newIndent + , context = context + , row = row + , col = col + } + + + +-- POSITION + + +{-| Code editors treat code like a grid. There are rows and columns. +In most editors, rows and colums are 1-indexed. You move to a new row +whenever you see a `\n` character. + +The `getPosition` parser succeeds with your current row and column +within the string you are parsing. +-} +getPosition : Parser (Int, Int) +getPosition = + I.Parser <| \state -> I.Good (state.row, state.col) state + + +{-| The `getRow` parser succeeds with your current row within +the string you are parsing. +-} +getRow : Parser Int +getRow = + I.Parser <| \state -> I.Good state.row state + + +{-| The `getCol` parser succeeds with your current column within +the string you are parsing. +-} +getCol : Parser Int +getCol = + I.Parser <| \state -> I.Good state.col state + + +{-| Editors think of code as a grid, but behind the scenes it is just +a flat array of UTF16 characters. `getOffset` tells you your index in +that flat array. So if you have read `"\n\n\n\n"` you are on row 5, +column 1, and offset 4. + +**Note:** browsers use UTF16 strings, so characters may be one or two 16-bit +words. This means you can read 4 characters, but your offset will move by 8. +-} +getOffset : Parser Int +getOffset = + I.Parser <| \state -> I.Good state.offset state + + +{-| Get the entire string you are parsing right now. Paired with +`getOffset` this can let you use `String.slice` to grab substrings +with very little intermediate allocation. +-} +getSource : Parser String +getSource = + I.Parser <| \state -> I.Good state.source state + diff --git a/finished/src/Data/Article.elm b/finished/src/Data/Article.elm index 0a4b8ec..c921d4b 100644 --- a/finished/src/Data/Article.elm +++ b/finished/src/Data/Article.elm @@ -3,15 +3,12 @@ module Data.Article ( Article , Body , Slug - , Tag , bodyToHtml , bodyToMarkdownString , decoder , decoderWithBody , slugParser , slugToString - , tagDecoder - , tagToString ) import Data.Article.Author as Author exposing (Author) @@ -110,24 +107,6 @@ slugToString (Slug slug) = --- TAGS -- - - -type Tag - = Tag String - - -tagToString : Tag -> String -tagToString (Tag slug) = - slug - - -tagDecoder : Decoder Tag -tagDecoder = - Decode.map Tag Decode.string - - - -- BODY -- diff --git a/finished/src/Data/Article/Tag.elm b/finished/src/Data/Article/Tag.elm new file mode 100644 index 0000000..9f0ee90 --- /dev/null +++ b/finished/src/Data/Article/Tag.elm @@ -0,0 +1,55 @@ +module Data.Article.Tag + exposing + ( Tag + , decoder + , encode + , listParser + , toString + ) + +import Json.Decode as Decode exposing (Decoder) +import Json.Encode as Encode exposing (Value) +import Parser exposing ((|.), (|=), Parser, end, ignore, keep, oneOrMore, repeat, zeroOrMore) + + +type Tag + = Tag String + + +toString : Tag -> String +toString (Tag str) = + str + + +encode : Tag -> Value +encode (Tag str) = + Encode.string str + + +decoder : Decoder Tag +decoder = + Decode.map Tag Decode.string + + +listParser : Parser (List Tag) +listParser = + Parser.succeed (List.map Tag) + |. ignore zeroOrMore isWhitespace + |= repeat zeroOrMore tag + |. end + + + +-- INTERNAL -- + + +tag : Parser String +tag = + keep oneOrMore (\char -> not (isWhitespace char)) + |. ignore zeroOrMore isWhitespace + + +isWhitespace : Char -> Bool +isWhitespace char = + -- Treat hashtags and commas as effectively whitespace; ignore them. + char == '#' || char == ',' || char == ' ' diff --git a/finished/src/Page/Article/Editor.elm b/finished/src/Page/Article/Editor.elm index 7e6d802..2f9274b 100644 --- a/finished/src/Page/Article/Editor.elm +++ b/finished/src/Page/Article/Editor.elm @@ -1,6 +1,7 @@ module Page.Article.Editor exposing (Model, Msg, initEdit, initNew, update, view) import Data.Article as Article exposing (Article, Body) +import Data.Article.Tag as Tag exposing (Tag) import Data.Session exposing (Session) import Data.User exposing (User) import Html exposing (..) @@ -8,6 +9,7 @@ import Html.Attributes exposing (attribute, class, defaultValue, disabled, href, import Html.Events exposing (onInput, onSubmit) import Http import Page.Errored exposing (PageLoadError, pageLoadError) +import Parser import Request.Article import Route import Task exposing (Task) @@ -26,7 +28,7 @@ type alias Model = , title : String , body : String , description : String - , tags : List String + , tags : String , isSaving : Bool } @@ -38,7 +40,7 @@ initNew = , title = "" , body = "" , description = "" - , tags = [] + , tags = "" , isSaving = False } @@ -60,7 +62,7 @@ initEdit session slug = , title = article.title , body = Article.bodyToMarkdownString article.body , description = article.description - , tags = article.tags + , tags = String.join " " article.tags , isSaving = False } ) @@ -121,7 +123,7 @@ viewForm model = , Form.input [ placeholder "Enter tags" , onInput SetTags - , defaultValue (String.join " " model.tags) + , defaultValue model.tags ] [] , button [ class "btn btn-lg pull-xs-right btn-primary", disabled model.isSaving ] @@ -152,10 +154,24 @@ update user msg model = [] -> case model.editingArticle of Nothing -> - user.token - |> Request.Article.create model - |> Http.send CreateCompleted - |> pair { model | errors = [], isSaving = True } + case Parser.run Tag.listParser model.tags of + Ok tags -> + let + request = + Request.Article.create + { tags = tags + , title = model.title + , body = model.body + , description = model.description + } + user.token + in + request + |> Http.send CreateCompleted + |> pair { model | errors = [], isSaving = True } + + Err _ -> + ( { model | errors = [ ( Tags, "Invalid tags." ) ] }, Cmd.none ) Just slug -> user.token @@ -173,7 +189,7 @@ update user msg model = ( { model | description = description }, Cmd.none ) SetTags tags -> - ( { model | tags = tagsFromString tags }, Cmd.none ) + ( { model | tags = tags }, Cmd.none ) SetBody body -> ( { model | body = body }, Cmd.none ) @@ -217,6 +233,7 @@ type Field = Form | Title | Body + | Tags type alias Error = @@ -235,14 +252,6 @@ modelValidator = -- INTERNAL -- -tagsFromString : String -> List String -tagsFromString str = - str - |> String.split " " - |> List.map String.trim - |> List.filter (not << String.isEmpty) - - redirectToArticle : Article.Slug -> Cmd msg redirectToArticle = Route.modifyUrl << Route.Article diff --git a/finished/src/Page/Home.elm b/finished/src/Page/Home.elm index ffc4995..2b04a6c 100644 --- a/finished/src/Page/Home.elm +++ b/finished/src/Page/Home.elm @@ -3,7 +3,7 @@ module Page.Home exposing (Model, Msg, init, update, view) {-| The homepage. You can get here via either the / or /#/ routes. -} -import Data.Article as Article exposing (Tag) +import Data.Article.Tag as Tag exposing (Tag) import Data.Session exposing (Session) import Html exposing (..) import Html.Attributes exposing (attribute, class, classList, href, id, placeholder) @@ -100,7 +100,7 @@ viewTag tagName = , href "javascript:void(0)" , onClick (SelectTag tagName) ] - [ text (Article.tagToString tagName) ] + [ text (Tag.toString tagName) ] diff --git a/finished/src/Request/Article.elm b/finished/src/Request/Article.elm index 25d8aaf..8f6fea2 100644 --- a/finished/src/Request/Article.elm +++ b/finished/src/Request/Article.elm @@ -14,8 +14,9 @@ module Request.Article , update ) -import Data.Article as Article exposing (Article, Body, Tag, slugToString) +import Data.Article as Article exposing (Article, Body, slugToString) import Data.Article.Feed as Feed exposing (Feed) +import Data.Article.Tag as Tag exposing (Tag) import Data.AuthToken exposing (AuthToken, withAuthorization) import Data.User as User exposing (Username) import Http @@ -68,7 +69,7 @@ defaultListConfig = list : ListConfig -> Maybe AuthToken -> Http.Request Feed list config maybeToken = - [ ( "tag", Maybe.map Article.tagToString config.tag ) + [ ( "tag", Maybe.map Tag.toString config.tag ) , ( "author", Maybe.map User.usernameToString config.author ) , ( "favorited", Maybe.map User.usernameToString config.favorited ) , ( "limit", Just (toString config.limit) ) @@ -114,7 +115,7 @@ feed config token = tags : Http.Request (List Tag) tags = - Decode.field "tags" (Decode.list Article.tagDecoder) + Decode.field "tags" (Decode.list Tag.decoder) |> Http.get (apiUrl "/tags") @@ -169,7 +170,7 @@ type alias CreateConfig record = | title : String , description : String , body : String - , tags : List String + , tags : List Tag } @@ -194,7 +195,7 @@ create config token = [ ( "title", Encode.string config.title ) , ( "description", Encode.string config.description ) , ( "body", Encode.string config.body ) - , ( "tagList", Encode.list (List.map Encode.string config.tags) ) + , ( "tagList", Encode.list (List.map Tag.encode config.tags) ) ] body = diff --git a/finished/src/Request/Article/Comments.elm b/finished/src/Request/Article/Comments.elm index 87501c5..0f7343b 100644 --- a/finished/src/Request/Article/Comments.elm +++ b/finished/src/Request/Article/Comments.elm @@ -1,6 +1,6 @@ module Request.Article.Comments exposing (delete, list, post) -import Data.Article as Article exposing (Article, Tag, slugToString) +import Data.Article as Article exposing (Article, slugToString) import Data.Article.Comment as Comment exposing (Comment, CommentId) import Data.AuthToken exposing (AuthToken, withAuthorization) import Http diff --git a/finished/src/Views/Article/Feed.elm b/finished/src/Views/Article/Feed.elm index ce3fd8c..6936c88 100644 --- a/finished/src/Views/Article/Feed.elm +++ b/finished/src/Views/Article/Feed.elm @@ -16,8 +16,9 @@ overkill, so we use simpler APIs instead. -} -import Data.Article as Article exposing (Article, Tag) +import Data.Article as Article exposing (Article) import Data.Article.Feed exposing (Feed) +import Data.Article.Tag as Tag exposing (Tag) import Data.AuthToken exposing (AuthToken) import Data.Session exposing (Session) import Data.User exposing (Username) @@ -126,7 +127,7 @@ sourceName source = "Global Feed" TagFeed tagName -> - "#" ++ Article.tagToString tagName + "#" ++ Tag.toString tagName FavoritedFeed username -> "Favorited Articles"