├── .firebaserc ├── .gitignore ├── LICENSE ├── README.md ├── _docs ├── animation.md ├── arguments.md ├── cheat-sheet.md ├── css-tricks.md ├── getting-started.md ├── graphics-circles.png ├── graphics.md ├── index.md ├── recursion.md └── romanesco.jpg ├── css ├── codemirror.css └── style.css ├── deploy.sh ├── embed.html ├── firebase.json ├── gallery ├── -L0jGl9IhooqRuTF9wxS.jpg ├── -L0jT5zaERgBPaf3P6LP.jpg ├── -L0kGLkGKVe9Iuid9jzC.jpg ├── -L0l7fV1tlR0Gobb64AM.jpg ├── -L0pQ_yU-SGVmDoRMfsF.jpg ├── -L0tTX3tlVpqHX6Umym4.jpg ├── -L42j_5BckaAOv8BICMj.jpg ├── -L4uJCV9VMZPOCBJ6ysM.jpg ├── -L4uLB99JzFHMPcVoiTm.jpg ├── -L4uLkY2Yh6GE9_nE6xn.jpg ├── -L4uMMohcfHFX_pBOr6l.jpg ├── -L4uN8VinvdmaAJqMNHJ.jpg ├── -L4uNdoRFf9FHpsDA1mc.jpg ├── -L4uNyhbwpSeFdB0_2J4.jpg ├── -L4uOhQIyny6CWcP9yvX.jpg ├── -L4uP84R9XYlbOomuPl0.jpg ├── -L4uPRjyEV237kNmUX5v.jpg └── -L4uQH3RqD3QOSbxHhNE.jpg ├── index.html ├── js ├── core.js └── editor.js └── third_party ├── bundle.min.js ├── codemirror.min.js ├── marked.min.js ├── preact-router.min.js ├── preact.min.js └── seedrandom.min.js /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "emrg-pcg" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | _build 4 | node_modules 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present, EMRG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Seed 2 | 3 | Procedural Content Generator 4 | 5 | 6 | ## Local development 7 | 8 | The app runs as a single-page application, so we use [serve](https://www.npmjs.com/package/serve) to always serve the index.html. 9 | 10 | # Only needed the first time 11 | npm install -g serve 12 | 13 | # Serve as a single-page application 14 | serve -s 15 | 16 | ## Deploy 17 | Do this once: 18 | 19 | npm install -g firebase-tools 20 | firebase login 21 | 22 | Do this every time you want to deploy: 23 | 24 | ./deploy.sh 25 | 26 | This will copy all the files over to the _build directory, and a timestamp to the CSS and JS files so we immediately see the latest version. 27 | -------------------------------------------------------------------------------- /_docs/animation.md: -------------------------------------------------------------------------------- 1 | # Animation 2 | 3 | You can create simple animations by specifying the start and end value between square brackets [ ] separated by a comma. Seed will animate over a period of 2 seconds (by default) from the start to the end value and back. **Press "Play" to start the animation**: 4 | 5 | 6 | 7 | 8 | You can interpolate the color using RGB or HSL values: 9 | 10 | 11 | 12 | Note that we can't shift the *phase* of the animation. All shapes move at the same rate and start and end at the same position in time. This is a limitation of the current system. 13 | 14 | SVG has a number of interesting properties to animate. For example, you can animate the `dashoffset` to "build up" a line (press "Play" to start): 15 | 16 | 17 | 18 | To change the duration of an animation you can use the directive `%duration` as shown belown. You can specify the length in seconds `s` or in milliseconds `ms`. By default Seed uses an `%animation` setting of the `bounce` type, meaning it takes half the duration to move from beginning to end, and the other half to go back. You can also set the animation to the type `linear`, in that case it will move from beginning to end and return immediately to the beginning. Finally, if you set the animation to the type `once`, it will act the same way as with `bounce`, but the animation will finish as soon as it has ended the first time. You can try these settings by testing the animation below: 19 | 20 | -------------------------------------------------------------------------------- /_docs/arguments.md: -------------------------------------------------------------------------------- 1 | # Working with arguments 2 | 3 | Lets's have a look at a simple animation of a group of circles falling to the ground. We can have a few copies of them: 4 | 5 | 6 | Suppose we want to add a specific css style called animation-delay to break the synchonicity of the example. We could refer to it by using a range. Which will result in a random nummer for each circle. 7 | 8 | 9 | We can also send an argument and use that to obtain an animation-delay for each group of circles. 10 | 11 | 12 | With a little more additions we can recreate something like the matrix rain effect (in red): 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /_docs/cheat-sheet.md: -------------------------------------------------------------------------------- 1 | # Cheat Sheet 2 | 3 | ## Basic Elements 4 | 5 | A **block** contains a set of options which Seed can pick from: 6 | 7 | ```seed 8 | root: 9 | - Dave 10 | - Edna 11 | - Mark 12 | ``` 13 | 14 | An **option** starts with `"- "` and can continue over multiple lines as long as it has 2 spaces in front of it: 15 | 16 | ```seed 17 | root: 18 | - This is a very long piece of text. 19 | Note that no extra spaces are inserted between. 20 | We need to add them ourselves 21 | by appending another space to the line. 22 | ``` 23 | 24 | A **token** will be replaced with a random choice from the block it refers to: 25 | 26 | ```seed 27 | root: 28 | - Welcome to {{ place }}! 29 | 30 | place: 31 | - Paris 32 | - New Orleans 33 | - Johannesburg 34 | - Antwerp 35 | ``` 36 | 37 | A **global variable** will be inserted each time it is called again: 38 | 39 | ```seed 40 | root: 41 | - {{ letter:str }} -- {{ letter:str }} -- {{ letter:str }} 42 | 43 | letter: 44 | - A 45 | - B 46 | - C 47 | - D 48 | ``` 49 | 50 | Passing **arguments** so that content can be used multiple times: 51 | 52 | ```seed 53 | root: 54 | - {{ order(verb) }} 55 | 56 | order(action): 57 | - {{ action }} the doctor. {{ action }} the ambulance. 58 | - {{ action }} the police. {{ action }} the fire department. 59 | - {{ action }} my mother. {{ action }} my father. 60 | 61 | verb: 62 | - Call 63 | - Notify 64 | - Report to 65 | ``` 66 | 67 | 68 | ## Repetition 69 | 70 | Tokens can be repeated by adding a `|repeat()` filter: 71 | 72 | ```seed 73 | root: 74 | - {{ letter|repeat(10) }} 75 | 76 | letter: 77 | - A 78 | - B 79 | - C 80 | - D 81 | ``` 82 | 83 | You can also use [recursion](/docs/recursion) so a block includes itself: 84 | 85 | ```seed 86 | root: 87 | - A {{ root }} 88 | ``` 89 | 90 | ## Ranges 91 | 92 | You can pick from a numeric range using `..`: 93 | 94 | ```seed 95 | root: 96 | - You are {{ 5..95 }} years old. 97 | ``` 98 | 99 | You can also use this for character ranges: 100 | 101 | ```seed 102 | root: 103 | - {{ A..Z }} 104 | ``` 105 | 106 | Of course you can combine that with repetitions: 107 | 108 | ```seed 109 | root: 110 | - {{ A..Z|repeat(10) }} 111 | ``` 112 | 113 | ## Animation 114 | 115 | You can animate between two values using `[min, max]`: 116 | 117 | ```seed 118 | root: 119 | - {{ [100, 300] }} 120 | ``` 121 | 122 | The value will animate back and forward between the two values over a period of 2 seconds. 123 | 124 | ## Filters 125 | 126 | To change the result of a token add a filter using the `|`. A filter can be used to transform the text: 127 | 128 | ```seed 129 | root: 130 | - {{ stop_word|sentence }} it begins {{ stop_word }} it ends. 131 | 132 | stop_word: 133 | - so 134 | - and thus 135 | - as such 136 | ``` 137 | 138 | Besides the `repeat` filter there are also an `int` filter (attempting to transform a string into an integer value), a `float` filter (attempting to transform a string into a real value) and a `str` filter (transforms an integer or real value into a string value). Other supported filters that operate on strings are `upper`, `lower`, `title` and `sentence`. 139 | 140 | ## SVG Graphics 141 | 142 | [Documentation](/docs/generating-graphics) | [SVG Reference](https://developer.mozilla.org/en-US/docs/Web/SVG) 143 | 144 | To create graphics use a `` tag containing width and height attributes: 145 | 146 | ```seed 147 | root: 148 | - {{ shape|repeat(20) }} 149 | 150 | shape: 151 | - 152 | ``` 153 | 154 | To start in the middle of the composition, add a group with a transform to half the width/height: 155 | 156 | ```seed 157 | root: 158 | - 159 | {{ shape|repeat(25) }} 160 | 161 | 162 | shape: 163 | - 164 | ``` 165 | 166 | A **background** is a rectangle with width / height set to 100%: 167 | 168 | ```seed 169 | root: 170 | - {{ background }}{{ shape }} 171 | 172 | background: 173 | - 174 | 175 | shape: 176 | - 177 | ``` 178 | -------------------------------------------------------------------------------- /_docs/css-tricks.md: -------------------------------------------------------------------------------- 1 | # CSS tricks 2 | 3 | Since Seed is browser based, we can use a lot of CSS functionality. Below are a few examples on using things like **flex, grid or animate and fontawesome**: 4 | 5 | In the examples section is a fairly simple example of [Flex](https://developer.mozilla.org/en-US/docs/Web/CSS/flex) in CSS. The example pulls a number of pictures from cats and shows them in a flexible layout. The trick is in referring to style in the container div structure like this: `style="display: flex; flex-wrap:wrap;"` 6 | 7 | 8 | 9 | an example on [CSS grid](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout) works in a similar way. The grid creates a layout on which a system of rotated [truchet tiles](https://en.wikipedia.org/wiki/Truchet_tiles) is placed. 10 | 11 | 12 | Seed also allows to use CSS from somewhere else. To increase animation options we can use a range of predefined animations and import them over a link tag. [Animate.css](https://github.com/daneden/animate.css) is an example of such a stylesheet. We can point to a version hosted on a cdn ([content delivery network](https://en.wikipedia.org/wiki/Content_delivery_network)) like this: 13 | 14 | 15 | A similar example on using external CSS: use [font awesome](http://fontawesome.io/cheatsheet/) css. Again we can point to a version on a cdn: 16 | 17 | 18 | 19 | not happy with the provided interface? Stretch it to the limit :) 20 | 21 | -------------------------------------------------------------------------------- /_docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Seed generates new, random content based on a set of _rules_ you define. These rules always generate text. But because we're on the web this text can also be HTML code or SVG graphics. Seed uses a system called a [context-free grammar](https://en.wikipedia.org/wiki/Context-free_grammar). 4 | 5 | Here is a simple example that generates "thank you" notes (it is also displayed when [creating a new sketch](/sketch)): 6 | 7 | 8 | 9 | You can generate new variations using the "Generate" button (try it now!). Each variation has a unique _seed_, which consists of three letters. You can browse through the different variations using the arrows next to the seed value in the toolbar. 10 | 11 | Saving in Seed is easy: each time you save a sketch it generates a new, unique URL that you can share. Other people can make variations and save their versions. This makes it easy to experiment. We're adding user logins soon so you can save your favorite sketches to your account. 12 | 13 | ## The Structure of a Sketch 14 | 15 | A sketch in Seed consists of a number of "blocks". Each block starts with a name ending in a ":", and a list of options. If Seed encounters a `{{ token }}` it will look for a block with this name, pick an option from the list and replace the token with the selected option. 16 | 17 | The `root:` element is special. Seed will always look for a `root:` element when generating new content. This is the _start_ of your document. Beneath this line there are two options that start with a `-`. When generating content, Seed will _choose one of these options_. The more options you give, the more freedom the system has to choose between. 18 | 19 | > Create a list of options and Seed will choose one of them at random. 20 | 21 | Each time Seed encounters this list it can make a different choice. The choice is random, but the variation is based on the _random seed_. We'll talk about random seeds later. 22 | 23 | If this is all the system could do, we would be limited. We need a way to replace _parts_ of the text with other options. That's where _tokens_ come in. 24 | 25 | If we look at the first line below `root:` we see that it has these special `{{ }}` brackets in the middle of the sentence. We call these **tokens**: special markers that signal to Seed that we want to replace them with random content. Which content depends on what's between the tokens: the _identifier_. In this case the identifier is a name. We're going to lookup this name, choose from one of its options and place the result back into the text. 26 | 27 | > Use `{{ tokens }}` if you want Seed to fill in a random option. 28 | 29 | ## An Example 30 | 31 | Let's run through an example. We'll pretend we are the computer and will try to evaluate this sketch and return a result. First, we'll take a look at the root: 32 | 33 | ``` 34 | root: 35 | - Dear {{ giver }}, thank you for the {{ object }}. 36 | - Hey {{ giver }}, thanks for the {{ object }}! 37 | ``` 38 | 39 | Here, we can pick two options. This selection happens at random, so let's say we picked the first option. Now our text looks like this: 40 | 41 | ``` 42 | Dear {{ giver }}, thank you for the {{ object }}. 43 | ``` 44 | 45 | (Note that the `-` in the beginning is not part of the text and is stripped away.) 46 | 47 | We run through this piece of text, looking for tokens that we can replace. The first token we encounter is `{{ giver }}`. We look for a block starting with `giver:` and find it in the document: 48 | 49 | ``` 50 | giver: 51 | - Aunt Emma 52 | - Dave and Edna 53 | - Uncle Bob 54 | ``` 55 | 56 | The `giver` block has three options so again we pick one at random. Here, we choose option 3: "Uncle Bob". We place this in the text. We now have: 57 | 58 | ``` 59 | Dear Uncle Bob, thank you for the {{ object }}. 60 | ``` 61 | 62 | We still have one replacement to do, the `{{ object }}`. Again we go looking for a `object:` block: 63 | 64 | ``` 65 | object: 66 | - purple vase 67 | - golden retriever 68 | - dishwasher 69 | ``` 70 | 71 | Here we pick option 2, the golden retriever. Note that this block could _also_ have tokens! For example we could imagine that we want the material to be random, so we could say we don't just have a golden retriever, but a `{{ material }} retriever`. `material` is an identifier that points to a new block that has `golden`, `silver`, `bronze`, and so on. 72 | 73 | We now have everything to assemble the final sentence: 74 | 75 | ``` 76 | Dear Uncle Bob, thank you for the golden retriever. 77 | ``` 78 | 79 | ## Passing arguments 80 | 81 | Optionally, a block can also take one or more arguments. You can use the values of these arguments each time you need them in your options. The arguments, or parameters, are defined between parentheses `(arg1, arg2, ...)` and separated by commas, before the `:` sign. So if for instance we have a block defined as `givethanks(adj):` and we call it with a value of, for example, 'shiny' `{{ givethanks('shiny') }}`, a potential option in `givethanks` written as `a {{ adj }} dishwasher`, will be replaced as 'a shiny dishwasher'. The value of an argument is only available to the current block, if you want to use in a subsequent block (through recursion or composition), you will need to pass it again. 82 | 83 | 84 | 85 | 86 | ## More Complex Examples 87 | 88 | This is enough to do simple text generation. By making our replacements more complex, we can create huge grammar files. For example here is a Immanuel Kant philosophy generator: 89 | 90 | 91 | 92 | Note that this uses the exact same system as in this simple example. We just have _much more_ options to choose from, and each of those contain choices as well, and so on. 93 | 94 | It's interesting to see how this is built up. Because we want the text to have multiple sentences, we render a number of `{{ sentence }}` tokens in the root: 95 | 96 | ``` 97 | root: 98 | -

{{ sentence }}

{{ sentence }}

{{ sentence }}

99 | - ...more content here... 100 | ``` 101 | 102 | Each of those sentence tokens will be replaced with a random sentence. Every time we evaluate this the system will choose a different option. Because this is random it's possible that the system will accidentally pick the same one twice. Just choose a different variation if that happens. 103 | -------------------------------------------------------------------------------- /_docs/graphics-circles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/_docs/graphics-circles.png -------------------------------------------------------------------------------- /_docs/graphics.md: -------------------------------------------------------------------------------- 1 | # Generating Graphics 2 | 3 | **Seed generates only text.** It has no knowledge of graphical shapes, composition or color. However, we can "cheat" the system by realizing that everything we see on a web page is also text: it's just HTML code. And not just HTML code: webpages can also integrate [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics), which is short for Scaleable Vector Graphics. In other words, _to generate graphics, we will generate SVG code_. 4 | 5 | Here's a small example: 6 | 7 | 8 | 9 | Let's look at the actual code we generate. The `root:` block has only one option which generates a `` tag. This tag contains properties for the `width` and `height` of the composition. We choose 300 here but you might choose different values depending on the size of your screen. The width and height are not absolutely required but not setting them will result in a tiny SVG document, which is not really useful. Inside of this tag we generate a `{{ circle|repeat(50) }}`. This expands to 50 `{{ circle }}` blocks. In other words, we generate 50 circles. 10 | 11 | The `circle:` block consists of a single option with a [<circle>](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle) tag. This a standard SVG element that has a number of properties: 12 | 13 | * **cx** (short for "center X") defines the horizontal position of the center of the circle. 14 | * **cy** (short for "center Y") defines the vertical position of the center of the circle. 15 | * **r** (short for "radius") defines the radius of the circle. Note that this is the radius — _not_ the diameter — so the circle will be twice as big as this. 16 | * **fill** defines the fill color of the circle. There are many ways in which we can [specify colors](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Fills_and_Strokes): using predefined names, through RGB or HSL values, or using hexadecimal codes. If you're familiar with CSS colors, this is exactly the same. 17 | * **stroke** defines the stroke color of the circle. We use a stroke color here so the overlapping circles don't turn into a big red blob. If we want we can also specify the `stroke-width` to set the line width of the stroke. 18 | 19 | What else can we generate? SVG has support for lines, paths, rectangles, circles and text. We will show some examples below but you can easily find [more documentation on SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Basic_Shapes). 20 | 21 | ## Lines 22 | 23 | Here's an example with the `` element: 24 | 25 | 26 | 27 | A `` has the following attributes: 28 | 29 | * **x1**: the starting horizontal position of the line. 30 | * **y1**: the starting vertical position of the line. 31 | * **x2**: the ending horizontal position of the line. 32 | * **y2**: the ending vertical position of the line. 33 | 34 | A line has no fill color but the stroke argument is **required** (otherwise the line won't show up). 35 | 36 | Note that this also shows how to set a "background": just draw a rectangle the size of your composition. The easiest way is to set the width and height to 100%. 37 | 38 | ## Rectangles 39 | 40 | We've already seen rectangles used as the background element. Here we use them to create a "city": 41 | 42 | 43 | 44 | A `` has the following attributes: 45 | 46 | * **x**: the horizontal position of the rectangle. 47 | * **y**: the vertical position of the rectangle. 48 | * **width**: the width of the rectangle. 49 | * **width**: the height of the rectangle. 50 | * **rx**: the horizontal corner radius of the rectangle. 51 | * **ry**: the vertical corner radius of the rectangle (if you leave this off it will be the same as `rx`). 52 | 53 | Note that rectangles (and ``s) always draw from the top left coordinate. 54 | 55 | In this example we created rectangles for the building anchored to the bottom of the composition. However we can't set the y value fixed to 300 and use negative heights for the builing. Instead we use a CSS `transform` to rotate the entire composition 180 degrees so it's flipped upside-down. 56 | 57 | ## Circles 58 | 59 | Circles are a bit different since we specify their position from the center. 60 | 61 | 62 | 63 | This example uses two tricks: 64 | 65 | * It uses a [SVG viewBox](https://www.sarasoueidan.com/blog/svg-coordinate-systems/#viewbox-syntax) to make the coordinate space smaller. Even though the SVG size is 300 by 300, we use coordinates between 0 and 10. 66 | * To specify the radius it uses values between 0.1 and 0.9. To do that we start the value with "0." and then append the expression `{{ 1..9 }}`. This expands to `0.1` or `0.5` and so on. 67 | 68 | A `` has the following attributes: 69 | 70 | * **cx**: the horizontal position of the _center_ of the circle. 71 | * **cy**: the vertical position of the _center_ of the circle. 72 | * **r**: the radius of the circle. Note that this is the radius — _not_ the diameter — so the circle will be twice as big as this. 73 | 74 | ## Polygons 75 | 76 | Polygons represent a closed shape defined by a set of points. 77 | 78 | 79 | 80 | The `points` attribute of a polygon contains pairs of X/Y coordinates, e.g. `10,80 50,10 90,80`. To generate a random list we use an extra space in front of a coordinate pair to separate them (otherwise we would get `10,8050,1090,80`). 81 | 82 | A `` has the following attributes: 83 | 84 | * **points**: the list of coordinates in the form `x1,y1 x2,y2 x3,y3 ...`. The shape is closed automatically. 85 | 86 | ## Path 87 | 88 | Path is the most versatile and complex shape. It can generate straight lines, quadratic béziers, cubic béziers, and arcs. It uses an efficient description that uses single-letter commands and coordinates, e.g. `M20 40` means "move to coordinate 20,40". Coordinates can be specified in both _absolute_ and _relative_ mode using uppercase and lowercase letters, respectively. 89 | 90 | Here's an example using relative coordinates: 91 | 92 | 93 | 94 | Try to decrease the amount of paths generated (change `{{ path|repeat(300) }}` to `{{ path|repeat(3) }}`) to see how this works. Every path is one line, going down. To generate a line, we first move to the center using the `M` move to command: `M150,5`. We then use relative line command "`l`" to move down. Here we change the X position randomly but never the Y position. Y always goes down with a constant offset, but X can fluctuate, resulting in overlapping lines creating this structure. 95 | 96 | Here's an example that uses absolute coordinates. Again, try changing `{{ path|repeat(150) }}` to a smaller value and gradually increasing it to see what's going on: 97 | 98 | 99 | 100 | Here again we use a small viewBox to limit the coordinates to a grid. 101 | 102 | A `` has only one attribute: 103 | 104 | * **d**: the path data. Read the [SVG Path reference](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths) for more info. 105 | 106 | ## Text 107 | 108 | SVG text can be used for small text elements. It doesn't support line wrapping or more advanced features. Use absolutely-positioned `
`s for that. 109 | 110 | 111 | 112 | A `` element has the following attributes: 113 | 114 | * **x**: the horizontal position of the text. 115 | * **y**: the vertical position of the text. Text is always positioned on the baseline. 116 | * **font-size**: the font size of the text. 117 | * **text-anchor**: the alignment of text. Either `start`, `middle` or `end`. 118 | 119 | The text itself goes between the `` opening and closing tags. 120 | 121 | In addition there are methods for fitting text using `textLength` and `lengthAdjust` attributes. Read the [SVG text reference](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text) for more info. 122 | 123 | ## Colors: Fill and Stroke 124 | 125 | You can color your shape by giving it a fill color or stroke color. Many options are possible in SVG using patterns, images and so on. We will just list the basics here. [Read the SVG documentation](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Fills_and_Strokes) for more information and examples. 126 | 127 | * **fill**: the fill color of a shape. There are many ways in which we can [specify colors](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Fills_and_Strokes): using predefined names, through RGB or HSL values, or using hexadecimal codes. If you're familiar with CSS colors, this is exactly the same. A fill of `none` means the shape is not filled. 128 | * **stroke**: the stroke color of a shape. We can use the same colors as with fills. By default the stroke is `none`, meaning the shape is not stroked. 129 | * **stroke-width**: the line width of the stroke. This is always used in combination with `stroke`. If you don't specify a `stroke-width` it will have a default value of 1. 130 | * **stroke-linecap**: the shape at the end of a line: either `butt`, `square` or `round`. 131 | * **stroke-linejoin**: the shape when two segments connect: either `miter`, `round` or `bevel`. 132 | * **stroke-dasharray**: a pattern for dashed lines, as comma-seperated numbers, e.g. `"5,10,5"`. 133 | 134 | In addition, you can also specify the `mix-blend-mode` to use Photoshop-style blending modes. This is a CSS style property, so it needs to go in the style attribute. Using `stroke-dasharray` and blend modes you can create some pretty interesting effects: 135 | 136 | 137 | 138 | ## Colors: Gradients 139 | 140 | To use gradients in SVG you first have to define them in a `` section at the top of the document. Here you define if you want a `` or ``, the direction of the gradient and the color stops. then you can refer to them using a URL. 141 | 142 | 143 | 144 | Since we have to do this in two steps it difficult to generate a number of random gradients and have Seed refer to them. We're looking into a solution for this, using some sort of memory function so the system knows which gradients it has generated. 145 | 146 | ## Reusing elements 147 | 148 | You can store the contents of a generated element in a global variable by appending a `:` and a self chosen name. In the example below the two circles are stored in a global variable `{{ circles:cc }}` and inserted each time we call it again: 149 | 150 | 151 | 152 | A different way to reuse an element is to define it in the `` section, before other graphic elements. In the example below we defined a group `` containing two generated `circle` elements. Note that we gave the group an `id` attribute with a self chosen name: ``, we need this because we want to be able to reference it later. After the `` section, the `use` element takes this `id` attribute as the value for its `xlink:href` attribute. Note that we have to add a `#` before the attribute name. The `x` and `y` attributes of the `use` element are added to the original coordinates of the group, so that we can position them somewhere else: 153 | 154 | 155 | -------------------------------------------------------------------------------- /_docs/index.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | Welcome to the Seed procedural content generator! We've created a system to easily generate procedural content using web technologies. 4 | 5 | * [Read the Getting Started Guide](/docs/getting-started) 6 | * [Learn how to create graphics](/docs/graphics) 7 | * [Read the Cheat Sheet](/docs/cheat-sheet) 8 | 9 | --- 10 | 11 | ## Try Now 12 | 13 | It's easy to create a new sketch. Just click "New Sketch" in the top-right corner, or explore below: 14 | 15 | 16 | 17 | --- 18 | 19 | ## Questions 20 | 21 | We're setting up a forum for questions and cool examples. Meanwhile, email questions to info@emrg.be. 22 | -------------------------------------------------------------------------------- /_docs/recursion.md: -------------------------------------------------------------------------------- 1 | # Recursion 2 | 3 | In recursion, one part is defined in terms of itself. There are many examples of recursion in nature: think about trees, where smaller branches mirror the large-scale structure of the tree. Or think about the Romanesco broccoli, a vegetable that has a beautiful recursive shape. 4 | 5 | ![The fractal shape of a Romanesco broccoli](/_docs/romanesco.jpg)
6 | Image by [Jon Sullivan](https://commons.wikimedia.org/wiki/File:Fractal_Broccoli.jpg) 7 | 8 | In Seed we can build our own recursive shapes by letting a block refer to itself. This automatically creates multiple copies of the shape. Here's a simple example: 9 | 10 | 11 | 12 | If you look closely you can see that at the end of the `circle:` block we call `circle:`again! So a {{ circle }} expands to a `` tag and a {{ circle }}, which expands to a `` tag and a {{ circle }}, which expands to... you get the idea. This is a recursive process: a system that calls itself. Seed has a mechanism to stop at some point (by default after 50 iterations), otherwise our expansion would never end and the system would crash. 13 | 14 | Although this example certainly works to get many circles on screen, we would be better off to just use the repeat filter `|repeat(num)` as we've seen in the [generating graphics](/docs/graphics) chapter. Things get more interesting if every recursive shape changes a little bit: becomes bigger or smaller, rotates, etc. Like this: 15 | 16 | 17 | 18 | Let's go through this example to see what's happening. We start by setting up our basic shape in the root block. Then we call `{{ shape }}` which does the actual work. In `shape:` we call the `rect` block. This is just a simple rect, however note that it is centered around the middle: its width and height is set to 100, and its X/Y position is set to -50, meaning it will be drawn at the center (note that we translate our canvas in the root so we don't actually show this from the top-left corner). We do this so it becomes easier to handle rotations. 19 | 20 | The `rect:` block only draws a single rect. But the `shape:` block also creates a `` group that contains a transform. Inside of that group we draw the shape itself. This is the recursive part: a shape consists of a rectangle, and a transformed shape, which consists of a rectangle and a transfromed shape, which... 21 | 22 | Each copy will have the transformation applied to it, so each copy will be rotated 5 degrees, scaled to 90%, and shifted a bit to the bottom-right. Change the values in the transformation to see how they affect the shapes. 23 | 24 | We can use this principle to generate many interesting recursive shapes, like trees: 25 | 26 | 27 | 28 | ## Recursion depth 29 | 30 | By default, Seed uses a recursion depth of 50. This means blocks can call other blocks up until 50 levels deep. Since recursion could go on indefinitely, we have to limit the recursion depth somehow otherwise nothing ever would get rendered. It's possible to overrule the recursion depth however by using the `%depth` directive. In the example below the depth has been set to 20. See wat happens when you set it to a higher (or lower) value. 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /_docs/romanesco.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/_docs/romanesco.jpg -------------------------------------------------------------------------------- /css/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: monospace; 6 | height: 300px; 7 | color: black; 8 | direction: ltr; 9 | } 10 | 11 | /* PADDING */ 12 | 13 | .CodeMirror-lines { 14 | padding: 4px 0; /* Vertical padding around content */ 15 | } 16 | .CodeMirror pre { 17 | padding: 0 4px; /* Horizontal padding of content */ 18 | } 19 | 20 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 21 | background-color: white; /* The little square between H and V scrollbars */ 22 | } 23 | 24 | /* GUTTER */ 25 | 26 | .CodeMirror-gutters { 27 | border-right: 1px solid #ddd; 28 | background-color: #f7f7f7; 29 | white-space: nowrap; 30 | } 31 | .CodeMirror-linenumbers {} 32 | .CodeMirror-linenumber { 33 | padding: 0 3px 0 5px; 34 | min-width: 20px; 35 | text-align: right; 36 | color: #999; 37 | white-space: nowrap; 38 | } 39 | 40 | .CodeMirror-guttermarker { color: black; } 41 | .CodeMirror-guttermarker-subtle { color: #999; } 42 | 43 | /* CURSOR */ 44 | 45 | .CodeMirror-cursor { 46 | border-left: 1px solid black; 47 | border-right: none; 48 | width: 0; 49 | } 50 | /* Shown when moving in bi-directional text */ 51 | .CodeMirror div.CodeMirror-secondarycursor { 52 | border-left: 1px solid silver; 53 | } 54 | .cm-fat-cursor .CodeMirror-cursor { 55 | width: auto; 56 | border: 0 !important; 57 | background: #7e7; 58 | } 59 | .cm-fat-cursor div.CodeMirror-cursors { 60 | z-index: 1; 61 | } 62 | .cm-fat-cursor-mark { 63 | background-color: rgba(20, 255, 20, 0.5); 64 | -webkit-animation: blink 1.06s steps(1) infinite; 65 | -moz-animation: blink 1.06s steps(1) infinite; 66 | animation: blink 1.06s steps(1) infinite; 67 | } 68 | .cm-animate-fat-cursor { 69 | width: auto; 70 | border: 0; 71 | -webkit-animation: blink 1.06s steps(1) infinite; 72 | -moz-animation: blink 1.06s steps(1) infinite; 73 | animation: blink 1.06s steps(1) infinite; 74 | background-color: #7e7; 75 | } 76 | @-moz-keyframes blink { 77 | 0% {} 78 | 50% { background-color: transparent; } 79 | 100% {} 80 | } 81 | @-webkit-keyframes blink { 82 | 0% {} 83 | 50% { background-color: transparent; } 84 | 100% {} 85 | } 86 | @keyframes blink { 87 | 0% {} 88 | 50% { background-color: transparent; } 89 | 100% {} 90 | } 91 | 92 | /* Can style cursor different in overwrite (non-insert) mode */ 93 | .CodeMirror-overwrite .CodeMirror-cursor {} 94 | 95 | .cm-tab { display: inline-block; text-decoration: inherit; } 96 | 97 | .CodeMirror-rulers { 98 | position: absolute; 99 | left: 0; right: 0; top: -50px; bottom: -20px; 100 | overflow: hidden; 101 | } 102 | .CodeMirror-ruler { 103 | border-left: 1px solid #ccc; 104 | top: 0; bottom: 0; 105 | position: absolute; 106 | } 107 | 108 | /* DEFAULT THEME */ 109 | 110 | .cm-s-default .cm-header {color: blue;} 111 | .cm-s-default .cm-quote {color: #090;} 112 | .cm-negative {color: #d44;} 113 | .cm-positive {color: #292;} 114 | .cm-header, .cm-strong {font-weight: bold;} 115 | .cm-em {font-style: italic;} 116 | .cm-link {text-decoration: underline;} 117 | .cm-strikethrough {text-decoration: line-through;} 118 | 119 | .cm-s-default .cm-keyword {color: #708;} 120 | .cm-s-default .cm-atom {color: #219;} 121 | .cm-s-default .cm-number {color: #164;} 122 | .cm-s-default .cm-def {color: #00f;} 123 | .cm-s-default .cm-variable, 124 | .cm-s-default .cm-punctuation, 125 | .cm-s-default .cm-property, 126 | .cm-s-default .cm-operator {} 127 | .cm-s-default .cm-variable-2 {color: #05a;} 128 | .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} 129 | .cm-s-default .cm-comment {color: #a50;} 130 | .cm-s-default .cm-string {color: #a11;} 131 | .cm-s-default .cm-string-2 {color: #f50;} 132 | .cm-s-default .cm-meta {color: #555;} 133 | .cm-s-default .cm-qualifier {color: #555;} 134 | .cm-s-default .cm-builtin {color: #30a;} 135 | .cm-s-default .cm-bracket {color: #997;} 136 | .cm-s-default .cm-tag {color: #170;} 137 | .cm-s-default .cm-attribute {color: #00c;} 138 | .cm-s-default .cm-hr {color: #999;} 139 | .cm-s-default .cm-link {color: #00c;} 140 | 141 | .cm-s-default .cm-error {color: #f00;} 142 | .cm-invalidchar {color: #f00;} 143 | 144 | .CodeMirror-composing { border-bottom: 2px solid; } 145 | 146 | /* Default styles for common addons */ 147 | 148 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} 149 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} 150 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 151 | .CodeMirror-activeline-background {background: #e8f2ff;} 152 | 153 | /* STOP */ 154 | 155 | /* The rest of this file contains styles related to the mechanics of 156 | the editor. You probably shouldn't touch them. */ 157 | 158 | .CodeMirror { 159 | position: relative; 160 | overflow: hidden; 161 | background: white; 162 | } 163 | 164 | .CodeMirror-scroll { 165 | overflow: scroll !important; /* Things will break if this is overridden */ 166 | /* 30px is the magic margin used to hide the element's real scrollbars */ 167 | /* See overflow: hidden in .CodeMirror */ 168 | margin-bottom: -30px; margin-right: -30px; 169 | padding-bottom: 30px; 170 | height: 100%; 171 | outline: none; /* Prevent dragging from highlighting the element */ 172 | position: relative; 173 | } 174 | .CodeMirror-sizer { 175 | position: relative; 176 | border-right: 30px solid transparent; 177 | } 178 | 179 | /* The fake, visible scrollbars. Used to force redraw during scrolling 180 | before actual scrolling happens, thus preventing shaking and 181 | flickering artifacts. */ 182 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 183 | position: absolute; 184 | z-index: 6; 185 | display: none; 186 | } 187 | .CodeMirror-vscrollbar { 188 | right: 0; top: 0; 189 | overflow-x: hidden; 190 | overflow-y: scroll; 191 | } 192 | .CodeMirror-hscrollbar { 193 | bottom: 0; left: 0; 194 | overflow-y: hidden; 195 | overflow-x: scroll; 196 | } 197 | .CodeMirror-scrollbar-filler { 198 | right: 0; bottom: 0; 199 | } 200 | .CodeMirror-gutter-filler { 201 | left: 0; bottom: 0; 202 | } 203 | 204 | .CodeMirror-gutters { 205 | position: absolute; left: 0; top: 0; 206 | min-height: 100%; 207 | z-index: 3; 208 | } 209 | .CodeMirror-gutter { 210 | white-space: normal; 211 | height: 100%; 212 | display: inline-block; 213 | vertical-align: top; 214 | margin-bottom: -30px; 215 | } 216 | .CodeMirror-gutter-wrapper { 217 | position: absolute; 218 | z-index: 4; 219 | background: none !important; 220 | border: none !important; 221 | } 222 | .CodeMirror-gutter-background { 223 | position: absolute; 224 | top: 0; bottom: 0; 225 | z-index: 4; 226 | } 227 | .CodeMirror-gutter-elt { 228 | position: absolute; 229 | cursor: default; 230 | z-index: 4; 231 | } 232 | .CodeMirror-gutter-wrapper ::selection { background-color: transparent } 233 | .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } 234 | 235 | .CodeMirror-lines { 236 | cursor: text; 237 | min-height: 1px; /* prevents collapsing before first draw */ 238 | } 239 | .CodeMirror pre { 240 | /* Reset some styles that the rest of the page might have set */ 241 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 242 | border-width: 0; 243 | background: transparent; 244 | font-family: inherit; 245 | font-size: inherit; 246 | margin: 0; 247 | white-space: pre; 248 | word-wrap: normal; 249 | line-height: inherit; 250 | color: inherit; 251 | z-index: 2; 252 | position: relative; 253 | overflow: visible; 254 | -webkit-tap-highlight-color: transparent; 255 | -webkit-font-variant-ligatures: contextual; 256 | font-variant-ligatures: contextual; 257 | } 258 | .CodeMirror-wrap pre { 259 | word-wrap: break-word; 260 | white-space: pre-wrap; 261 | word-break: normal; 262 | } 263 | 264 | .CodeMirror-linebackground { 265 | position: absolute; 266 | left: 0; right: 0; top: 0; bottom: 0; 267 | z-index: 0; 268 | } 269 | 270 | .CodeMirror-linewidget { 271 | position: relative; 272 | z-index: 2; 273 | padding: 0.1px; /* Force widget margins to stay inside of the container */ 274 | } 275 | 276 | .CodeMirror-widget {} 277 | 278 | .CodeMirror-rtl pre { direction: rtl; } 279 | 280 | .CodeMirror-code { 281 | outline: none; 282 | } 283 | 284 | /* Force content-box sizing for the elements where we expect it */ 285 | .CodeMirror-scroll, 286 | .CodeMirror-sizer, 287 | .CodeMirror-gutter, 288 | .CodeMirror-gutters, 289 | .CodeMirror-linenumber { 290 | -moz-box-sizing: content-box; 291 | box-sizing: content-box; 292 | } 293 | 294 | .CodeMirror-measure { 295 | position: absolute; 296 | width: 100%; 297 | height: 0; 298 | overflow: hidden; 299 | visibility: hidden; 300 | } 301 | 302 | .CodeMirror-cursor { 303 | position: absolute; 304 | pointer-events: none; 305 | } 306 | .CodeMirror-measure pre { position: static; } 307 | 308 | div.CodeMirror-cursors { 309 | visibility: hidden; 310 | position: relative; 311 | z-index: 3; 312 | } 313 | div.CodeMirror-dragcursors { 314 | visibility: visible; 315 | } 316 | 317 | .CodeMirror-focused div.CodeMirror-cursors { 318 | visibility: visible; 319 | } 320 | 321 | .CodeMirror-selected { background: #d9d9d9; } 322 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 323 | .CodeMirror-crosshair { cursor: crosshair; } 324 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 325 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 326 | 327 | .cm-searching { 328 | background-color: #ffa; 329 | background-color: rgba(255, 255, 0, .4); 330 | } 331 | 332 | /* Used to force a border model for a node */ 333 | .cm-force-border { padding-right: .1px; } 334 | 335 | @media print { 336 | /* Hide the cursor when printing */ 337 | .CodeMirror div.CodeMirror-cursors { 338 | visibility: hidden; 339 | } 340 | } 341 | 342 | /* See issue #2901 */ 343 | .cm-tab-wrap-hack:after { content: ''; } 344 | 345 | /* Help users use markselection to safely style text background */ 346 | span.CodeMirror-selectedtext { background: none; } 347 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | /* Colors based on plantpower1 theme */ 2 | /* https://color.adobe.com/plantpower1-color-theme-10440009/ */ 3 | :root { 4 | --pink: #ff135c; 5 | --white: #fff; 6 | --green: #3a8828; 7 | --yellow: #fffd00; 8 | --greenish: #45f7b4; 9 | 10 | --header-height: 80px; 11 | } 12 | 13 | * { 14 | margin: 0; 15 | padding: 0; 16 | box-sizing: border-box; 17 | } 18 | 19 | img { 20 | max-width: 100%; 21 | } 22 | 23 | html, 24 | body { 25 | background: var(--white); 26 | margin: 0; 27 | } 28 | 29 | html.fullscreen, 30 | .fullscreen body { 31 | width: 100%; 32 | height: 100%; 33 | margin: 0; 34 | padding: 0; 35 | overflow: hidden; 36 | } 37 | 38 | body { 39 | font: 14px sans-serif; 40 | color: #444; 41 | } 42 | 43 | p { 44 | margin-bottom: 1rem; 45 | } 46 | 47 | a { 48 | text-decoration: none; 49 | color: inherit; 50 | } 51 | 52 | a.underline { 53 | text-decoration: underline; 54 | } 55 | 56 | /* Components */ 57 | 58 | .button { 59 | font-size: 1rem; 60 | font-family: inherit; 61 | line-height: 1.2; 62 | display: inline-block; 63 | outline: 0; 64 | padding: 9px 19px; 65 | margin: 0 10px 0 0; 66 | border-radius: 3px; 67 | border: 0; 68 | border-top: 2px solid transparent; 69 | border-bottom: 2px solid rgba(0, 0, 0, 0.2); 70 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 71 | cursor: pointer; 72 | white-space: nowrap; 73 | text-overflow: ellipsis; 74 | text-align: center; 75 | background: #555; 76 | color: white; 77 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); 78 | text-decoration: none; 79 | user-select: none; 80 | } 81 | 82 | .button:active { 83 | transform: translateY(1px); 84 | } 85 | 86 | /* App */ 87 | 88 | .app { 89 | width: 100%; 90 | height: 100%; 91 | display: flex; 92 | flex-direction: column; 93 | } 94 | 95 | /* Header */ 96 | 97 | .header { 98 | height: var(--header-height); 99 | display: flex; 100 | flex-direction: row; 101 | background: var(--pink); 102 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); 103 | border-bottom: 3px solid var(--green); 104 | color: #fff; 105 | width: 100%; 106 | z-index: 999; 107 | position: fixed; 108 | } 109 | 110 | .header__logo { 111 | margin-left: 1rem; 112 | flex: 1; 113 | align-self: center; 114 | font-size: 64px; 115 | padding: 5px 0; 116 | font-weight: 900; 117 | } 118 | 119 | .header__nav { 120 | align-self: center; 121 | justify-self: flex-end; 122 | } 123 | 124 | .header__nav .save-button { 125 | opacity: 0.2; 126 | } 127 | 128 | .header__nav .save-button.unsaved { 129 | background: #6a6; 130 | opacity: 1.0; 131 | } 132 | 133 | .header__nav .save-button.disabled { 134 | display: none; 135 | } 136 | 137 | /* Page */ 138 | 139 | .page { 140 | flex: 1; 141 | padding-top: var(--header-height); 142 | } 143 | 144 | .page.centered { 145 | display: flex; 146 | flex-direction: column; 147 | justify-content: center; 148 | align-items: center; 149 | } 150 | 151 | .page h1 { 152 | font-weight: 200; 153 | margin-bottom: 1rem; 154 | } 155 | 156 | .page a { 157 | padding-bottom: 2px; 158 | border-bottom: 1px solid rgba(255, 255, 255, 0.5); 159 | } 160 | 161 | .page a:hover { 162 | border-bottom-color: rgba(255, 255, 255, 0.9); 163 | } 164 | 165 | /* Home */ 166 | 167 | .intro { 168 | padding: 5rem 2rem; 169 | /* background: #fffd00; 170 | color: #333; 171 | border-bottom: 10px solid #3a8828; 172 | */ 173 | background: linear-gradient(#eee, #fff); 174 | } 175 | 176 | .intro__inner { 177 | max-width: 900px; 178 | margin: 0 auto; 179 | } 180 | 181 | .intro__large { 182 | font-size: 36px; 183 | font-weight: 200; 184 | } 185 | 186 | .intro__cta { 187 | width: 100%; 188 | text-align: center; 189 | margin: 3rem 0; 190 | display: flex; 191 | flex-direction: row; 192 | } 193 | 194 | .intro__cta .button { 195 | color: white; 196 | padding: 8px 12px; 197 | border-bottom: 2px solid rgba(0, 0, 0, 0.2); 198 | } 199 | 200 | .intro__cta .button:hover { 201 | border-bottom-color: rgba(0, 0, 0, 0.5); 202 | } 203 | 204 | .intro__cta .button.primary { 205 | background: var(--pink); 206 | } 207 | 208 | .gallery { 209 | background: white; 210 | } 211 | 212 | .gallery__inner { 213 | display: flex; 214 | flex-wrap: wrap; 215 | justify-content: center; 216 | max-width: 900px; 217 | margin: 0 auto; 218 | padding: 30px 0; 219 | } 220 | 221 | .gallery h1 { 222 | width: 100%; 223 | text-align: center; 224 | } 225 | 226 | .gallery__thumb { 227 | width: 250px; 228 | height: 250px; 229 | margin: 10px; 230 | } 231 | 232 | .gallery__img { 233 | border-radius: 3px; 234 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 235 | } 236 | 237 | .gallery__link { 238 | display: inline-block; 239 | border: 0 !important; 240 | } 241 | 242 | .gallery__link:active { 243 | transform: translateY(2px); 244 | } 245 | 246 | /* Editor */ 247 | 248 | .editor { 249 | flex: 1; 250 | display: flex; 251 | flex-direction: row; 252 | padding-top: var(--header-height); 253 | overflow: hidden; 254 | } 255 | 256 | .embed .editor { 257 | padding-top: 0; 258 | } 259 | 260 | .localversion { 261 | position: fixed; 262 | color: white; 263 | top: 0; 264 | right: 100px; 265 | line-height: var(--header-height); 266 | z-index: 1000; 267 | overflow: hidden; 268 | font-size: 12px; 269 | text-align: right; 270 | } 271 | 272 | .editor__source-wrap, 273 | .editor__viewer-wrap { 274 | width: 50%; 275 | display: flex; 276 | flex-direction: column; 277 | overflow: hidden; 278 | } 279 | 280 | .editor__toolbar { 281 | height: 50px; 282 | min-height: 50px; 283 | background: #333; 284 | display: flex; 285 | align-items: center; 286 | justify-content: flex-start; 287 | padding: 0 10px; 288 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.05); 289 | } 290 | 291 | .editor__toolbar .button { 292 | padding: 5px 8px; 293 | background: var(--pink); 294 | font-size: 0.8rem; 295 | } 296 | 297 | .editor__toolbar-left { 298 | flex: 1; 299 | } 300 | 301 | .editor__toolbar-center { 302 | flex: 1; 303 | text-align: left; 304 | } 305 | 306 | .editor__toolbar-right { 307 | flex: 1; 308 | text-align: right; 309 | } 310 | 311 | .editor__toolbar-right a { 312 | color: #eee; 313 | text-decoration: underline; 314 | opacity: 0.8; 315 | } 316 | 317 | .seed-picker { 318 | display: flex; 319 | } 320 | 321 | .seed-picker__prev { 322 | background: #777 !important; 323 | margin-right: 0; 324 | border-top-right-radius: 0; 325 | border-bottom-right-radius: 0; 326 | border-right: 1px solid rgba(255, 255, 255, 0.1); 327 | line-height: 12px; 328 | } 329 | 330 | .seed-picker__next { 331 | background: #777 !important; 332 | border-top-left-radius: 0; 333 | border-bottom-left-radius: 0; 334 | border-left: 1px solid rgba(255, 255, 255, 0.1); 335 | line-height: 12px; 336 | } 337 | 338 | .seed-picker__value { 339 | background: #777 !important; 340 | padding: 2px 4px; 341 | border: 0; 342 | border-top: 2px solid transparent; 343 | border-bottom: 2px solid rgba(0, 0, 0, 0.2); 344 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 345 | } 346 | 347 | .seed-picker__input { 348 | outline: none; 349 | font-size: 0.9rem; 350 | width: 50px; 351 | text-align: center; 352 | font-family: "SF Mono", Menlo, monospace; 353 | border: 0; 354 | background: transparent; 355 | color: white; 356 | } 357 | 358 | .editor__panels { 359 | flex: 1; 360 | display: flex; 361 | flex-direction: row; 362 | } 363 | 364 | .editor__source { 365 | height: 100%; 366 | flex: 1; 367 | display: flex; 368 | flex-direction: column; 369 | overflow: hidden; 370 | } 371 | 372 | .editor__debug { 373 | font-family: "SF Mono", Menlo, monospace; 374 | background: white; 375 | padding: 7px 10px; 376 | margin-right: 15px; 377 | position: relative; 378 | bottom: 0; 379 | z-index: 10; 380 | } 381 | 382 | .editor__area { 383 | width: 100%; 384 | height: 100%; 385 | color: inherit; 386 | background: none; 387 | outline: none; 388 | border: none; 389 | flex: 1; 390 | overflow: hidden; 391 | } 392 | 393 | .editor__area .CodeMirror { 394 | font-family: "SF Mono", Menlo, monospace; 395 | font-size: 11px; 396 | height: 100%; 397 | } 398 | 399 | .editor__area .CodeMirror-gutters { 400 | background-color: var(--white); 401 | border-right: 1px solid #eee; 402 | padding-right: 5px; 403 | } 404 | 405 | .editor__area .CodeMirror-linenumber { 406 | color: #ccc; 407 | } 408 | 409 | .editor__area .CodeMirror pre { 410 | padding: 0 8px; 411 | } 412 | 413 | .editor__viewer { 414 | overflow: auto; 415 | border-left: 2px solid #ddd; 416 | flex: 1; 417 | } 418 | 419 | .editor__result { 420 | padding: 10px; 421 | overflow-y: auto; 422 | position: relative; 423 | min-width: 100%; 424 | min-height: 100%; 425 | } 426 | 427 | /* Docs */ 428 | 429 | .docs { 430 | background: #f7f7f7; 431 | color: #444; 432 | display: flex; 433 | flex-direction: row; 434 | flex: 1; 435 | } 436 | 437 | .docs__nav { 438 | width: 300px; 439 | padding-top: 10px; 440 | position: fixed; 441 | } 442 | 443 | .docs__nav ul { 444 | list-style: none; 445 | } 446 | 447 | .docs__nav li { 448 | } 449 | 450 | .docs__nav li a { 451 | padding: 10px 15px; 452 | display: block; 453 | border-bottom: none !important; 454 | } 455 | 456 | .docs__nav li a.docs__header { 457 | border-bottom: 1px solid #ddd !important; 458 | padding-bottom: 15px; 459 | margin-bottom: 10px; 460 | } 461 | 462 | .docs__nav li a.active { 463 | font-weight: bold; 464 | color: #000; 465 | } 466 | 467 | .docs__body { 468 | flex: 1; 469 | min-height: 100%; 470 | padding: 20px 30px; 471 | max-width: 50rem; 472 | margin-left: 300px; 473 | border-left: 1px solid #ddd; 474 | } 475 | 476 | .app.view { 477 | position: fixed; 478 | top: 0; 479 | left: 0; 480 | width: 100%; 481 | height: 100%; 482 | display: flex; 483 | justify-content: center; 484 | align-items: center; 485 | background: black; 486 | } 487 | 488 | .app.view .docs__body { 489 | border: none; 490 | max-width: none; 491 | margin-left: 0; 492 | } 493 | 494 | .docs__body h2 { 495 | font-weight: 200; 496 | font-size: 1.2rem; 497 | line-height: 1.5; 498 | margin-bottom: 0.5rem; 499 | } 500 | 501 | .docs__body p, 502 | .docs__body li { 503 | line-height: 1.5; 504 | } 505 | 506 | .docs__body ul { 507 | margin-bottom: 1rem; 508 | } 509 | 510 | .docs__body li { 511 | margin-bottom: 0.5rem; 512 | } 513 | 514 | .docs__body hr { 515 | border: 0; 516 | border-bottom: 2px solid #ddd; 517 | margin-top: 2rem; 518 | margin-bottom: 2rem; 519 | } 520 | 521 | .docs__body img { 522 | height: 300px; 523 | border: 1px solid #ddd; 524 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); 525 | } 526 | 527 | .docs__body a { 528 | border-bottom-color: rgba(0, 0, 0, 0.2); 529 | } 530 | 531 | .docs__body a:hover { 532 | border-bottom-color: rgba(0, 0, 0, 0.9); 533 | } 534 | 535 | .docs__body a.noline { 536 | border-bottom: none !important; 537 | } 538 | 539 | .docs__body .code-wrap { 540 | display: flex; 541 | width: 100%; 542 | background: white; 543 | border-radius: 3px; 544 | margin-bottom: 1rem; 545 | } 546 | 547 | .docs__body pre { 548 | margin-bottom: 1rem; 549 | background: white; 550 | padding: 5px; 551 | color: #666; 552 | border-radius: 3px; 553 | font-size: 12px; 554 | } 555 | 556 | .docs__body .code-wrap pre { 557 | position: relative; 558 | flex: 1; 559 | margin-bottom: 0; 560 | padding-top: 0.5rem; 561 | padding-bottom: 0.5rem; 562 | } 563 | 564 | .docs__body .code-wrap pre::before { 565 | content: "sketch"; 566 | position: absolute; 567 | right: 7px; 568 | top: 5px; 569 | font-family: "SF Mono", Menlo, monospace; 570 | font-size: 11px; 571 | color: #ddd; 572 | text-transform: uppercase; 573 | } 574 | 575 | .docs__body .code-wrap .code-result { 576 | position: relative; 577 | width: 250px; 578 | padding: 1.4rem 10px; 579 | border-left: 1px solid #ddd; 580 | font-family: "SF Mono", Menlo, monospace; 581 | font-size: 12px; 582 | } 583 | 584 | .docs__body .code-wrap .code-result::before { 585 | content: "result"; 586 | position: absolute; 587 | right: 7px; 588 | top: 5px; 589 | font-size: 11px; 590 | color: #ddd; 591 | text-transform: uppercase; 592 | } 593 | 594 | .docs__body code { 595 | font-family: "SF Mono", Menlo, monospace; 596 | background: white; 597 | display: inline-block; 598 | border-radius: 3px; 599 | padding: 0 0.2rem; 600 | } 601 | 602 | .docs__body blockquote { 603 | background: var(--pink); 604 | color: white; 605 | padding: 1rem 1rem 0.2rem 1rem; 606 | font-size: 18px; 607 | border-radius: 10px; 608 | text-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 609 | margin-bottom: 1rem; 610 | } 611 | 612 | .docs__body blockquote code { 613 | color: white; 614 | background: transparent; 615 | } 616 | 617 | .docs__body iframe { 618 | border: 1px solid #ddd; 619 | margin-bottom: 1rem; 620 | width: 100%; 621 | height: 400px; 622 | border-radius: 3px; 623 | } 624 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | # Remove the build directory if it exists, then create a new one. 2 | rm -rf _build && mkdir _build 3 | 4 | # Copy all folders over to the _build directory. 5 | cp -R _docs css gallery js third_party _build 6 | 7 | # Get the current unix timestamp, then add it to all .js and .css references in index.html. 8 | TIMESTAMP=`date +%s` 9 | sed -e "s/\.js/\.js?$TIMESTAMP/" -e "s/\.css/\.css?$TIMESTAMP/" index.html > _build/index.html 10 | 11 | # Deploy to Firebase Hosting. 12 | firebase deploy 13 | -------------------------------------------------------------------------------- /embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Embed Example 6 | 7 | 8 | 9 |
10 | 11 | 12 | 42 | 43 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "_build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*" 7 | ], 8 | "rewrites": [{ 9 | "source": "**", 10 | "destination": "/index.html" 11 | }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /gallery/-L0jGl9IhooqRuTF9wxS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L0jGl9IhooqRuTF9wxS.jpg -------------------------------------------------------------------------------- /gallery/-L0jT5zaERgBPaf3P6LP.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L0jT5zaERgBPaf3P6LP.jpg -------------------------------------------------------------------------------- /gallery/-L0kGLkGKVe9Iuid9jzC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L0kGLkGKVe9Iuid9jzC.jpg -------------------------------------------------------------------------------- /gallery/-L0l7fV1tlR0Gobb64AM.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L0l7fV1tlR0Gobb64AM.jpg -------------------------------------------------------------------------------- /gallery/-L0pQ_yU-SGVmDoRMfsF.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L0pQ_yU-SGVmDoRMfsF.jpg -------------------------------------------------------------------------------- /gallery/-L0tTX3tlVpqHX6Umym4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L0tTX3tlVpqHX6Umym4.jpg -------------------------------------------------------------------------------- /gallery/-L42j_5BckaAOv8BICMj.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L42j_5BckaAOv8BICMj.jpg -------------------------------------------------------------------------------- /gallery/-L4uJCV9VMZPOCBJ6ysM.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L4uJCV9VMZPOCBJ6ysM.jpg -------------------------------------------------------------------------------- /gallery/-L4uLB99JzFHMPcVoiTm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L4uLB99JzFHMPcVoiTm.jpg -------------------------------------------------------------------------------- /gallery/-L4uLkY2Yh6GE9_nE6xn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L4uLkY2Yh6GE9_nE6xn.jpg -------------------------------------------------------------------------------- /gallery/-L4uMMohcfHFX_pBOr6l.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L4uMMohcfHFX_pBOr6l.jpg -------------------------------------------------------------------------------- /gallery/-L4uN8VinvdmaAJqMNHJ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L4uN8VinvdmaAJqMNHJ.jpg -------------------------------------------------------------------------------- /gallery/-L4uNdoRFf9FHpsDA1mc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L4uNdoRFf9FHpsDA1mc.jpg -------------------------------------------------------------------------------- /gallery/-L4uNyhbwpSeFdB0_2J4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L4uNyhbwpSeFdB0_2J4.jpg -------------------------------------------------------------------------------- /gallery/-L4uOhQIyny6CWcP9yvX.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L4uOhQIyny6CWcP9yvX.jpg -------------------------------------------------------------------------------- /gallery/-L4uP84R9XYlbOomuPl0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L4uP84R9XYlbOomuPl0.jpg -------------------------------------------------------------------------------- /gallery/-L4uPRjyEV237kNmUX5v.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L4uPRjyEV237kNmUX5v.jpg -------------------------------------------------------------------------------- /gallery/-L4uQH3RqD3QOSbxHhNE.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/seed/d7a87b0d966562b02be562b0f630a90f73c5f40d/gallery/-L4uQH3RqD3QOSbxHhNE.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Seed 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /js/core.js: -------------------------------------------------------------------------------- 1 | function rand(min, max) { 2 | return min + Math.random() * (max - min); 3 | } 4 | 5 | function choice(l) { 6 | const index = Math.floor(Math.random() * l.length); 7 | return l[index]; 8 | } 9 | 10 | function randomChar(min, max) { 11 | min = min.charCodeAt(0); 12 | max = max.charCodeAt(0); 13 | charCode = Math.round(rand(min, max)); 14 | return String.fromCharCode(charCode); 15 | } 16 | 17 | function randomTextSeed() { 18 | let seed = ''; 19 | for (let i = 0; i < 3; i++) { 20 | const min = 'A'.charCodeAt(0); 21 | const max = 'Z'.charCodeAt(0); 22 | seed += String.fromCharCode(Math.round(rand(min, max))); 23 | } 24 | return seed; 25 | } 26 | 27 | function seedTextToNumber(s) { 28 | let v = 0; 29 | for (let i = 0; i < s.length; i++) { 30 | v *= 26; 31 | const c = s.charCodeAt(i) - 65; 32 | v += c; 33 | } 34 | return v; 35 | } 36 | 37 | function seedNumberToText(v) { 38 | let s = ''; 39 | while (v > 0) { 40 | const digit = v % 26; 41 | const c = String.fromCharCode(65 + digit); 42 | s = c + s; 43 | v = Math.floor(v / 26); 44 | } 45 | return s; 46 | } 47 | 48 | function nextTextSeed(s) { 49 | const val = seedTextToNumber(s); 50 | return seedNumberToText(val + 1); 51 | } 52 | 53 | function prevTextSeed(s) { 54 | const val = seedTextToNumber(s); 55 | return seedNumberToText(val - 1); 56 | } 57 | 58 | function applyFilters(s, filters) { 59 | for (f of filters) { 60 | if (f === 'upper') { 61 | s = s.toUpperCase(); 62 | } else if (f === 'lower') { 63 | s = s.toLowerCase(); 64 | } else if (f === 'title') { 65 | s = s.toTitleCase(); 66 | } else if (f === 'sentence') { 67 | s = s.substring(0, 1).toUpperCase() + s.substring(1); 68 | } else { 69 | throw new Error(`Unknown filter "${f}".`); 70 | } 71 | } 72 | return s; 73 | } 74 | 75 | RegExp.escape = function(s) { 76 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 77 | }; 78 | 79 | function getURLParameter(name, url) { 80 | if (typeof window === 'undefined') { return false; } 81 | if (!url) { url = window.location.search; } 82 | const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), 83 | results = regex.exec(url); 84 | if (!results) { return null; } 85 | if (!results[2]) { return ''; } 86 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 87 | } 88 | 89 | String.prototype.toTitleCase = function () { 90 | return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); 91 | }; 92 | 93 | const VARIABLE_TAG_START = '{{'; 94 | const VARIABLE_TAG_END = '}}'; 95 | const REF_START = 'ref_start'; 96 | const REF_END = 'ref_end'; 97 | const TEXT = 'text'; 98 | const REF = 'ref'; 99 | const EOF = 'eof'; 100 | const KEY = 'key'; 101 | const STRING = 'string'; 102 | const REAL_CONST = 'real_const'; 103 | const INTEGER_CONST = 'integer_const'; 104 | const RANGE = 'range'; 105 | const ANIMATION_RANGE = 'anim_range'; 106 | const FILTER = 'filter'; 107 | const VAR_GLOBAL = 'var_g'; 108 | const PLUS = '+'; 109 | const MINUS = '-'; 110 | const MUL = '*'; 111 | const DIV = '/'; 112 | const LPAREN = '('; 113 | const RPAREN = ')'; 114 | const LBRACK = '['; 115 | const RBRACK = ']'; 116 | const COMMA = ','; 117 | const COLON = ':'; 118 | 119 | const DIGITS = '0123456789'; 120 | const ALPHA = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 121 | const ALPHANUM = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._'; 122 | const WHITESPACE = '  \t'; // todo: add more to these 123 | 124 | const PREAMBLE_RE = /^\s*(\w+)\s*:\s*(.+)*$/; 125 | const POS_INTEGER_RE = /^\d+$/; 126 | const DURATION_RE = /^(\d+(\.\d+)?)\s*(s|ms)?$/; 127 | const IMPORT_RE = /^\s*import\s+(.+)\s+as\s+(([a-zA-Z]|\_)([a-zA-Z0-9]|\_|\.(?!\.))*)\s*$/; 128 | 129 | const PREAMBLE_KEYS = ['depth', 'duration', 'animation', 'script']; 130 | const ANIMATION_TYPES = ['once', 'linear', 'bounce']; 131 | const MAX_LEVEL = 50; 132 | const TIMEOUT_MILLIS = 1000; 133 | 134 | const TIMEOUT = getURLParameter('timeout') !== 'false'; 135 | 136 | function bounce(t) { 137 | const a = t * Math.PI * 2; 138 | return 0.5 - Math.cos(a) * 0.5; 139 | } 140 | 141 | function lerp(min, max, t, animType) { 142 | switch (animType){ 143 | case 'linear': 144 | break; 145 | 146 | case 'bounce': 147 | default: 148 | t = bounce(t); 149 | break; 150 | } 151 | return min + t * (max - min); 152 | } 153 | 154 | class Token { 155 | constructor(type, value) { 156 | this.type = type; 157 | this.value = value; 158 | } 159 | 160 | toString() { 161 | return `Token(${this.type}, ${this.value})`; 162 | } 163 | } 164 | 165 | class Lexer { 166 | constructor(text) { 167 | this.text = text; 168 | this.pos = 0; 169 | this.currentChar = text.length > 0 ? text[this.pos] : null; 170 | } 171 | 172 | error(char) { 173 | const s = char.length > 1 ? 's' : ''; 174 | throw new Error(`Invalid character${s} ${char} at position ${this.pos} in phrase '${this.text}'.`); 175 | } 176 | 177 | advance() { 178 | this.pos += 1; 179 | if (this.pos > this.text.length - 1) { 180 | this.currentChar = null; 181 | } else { 182 | this.currentChar = this.text[this.pos]; 183 | } 184 | } 185 | 186 | peek() { 187 | const peekPos = this.pos + 1; 188 | if (peekPos > this.text.length - 1) { 189 | return null; 190 | } else { 191 | return this.text[peekPos]; 192 | } 193 | } 194 | 195 | skipWhitespace() { 196 | while (WHITESPACE.indexOf(this.currentChar) !== -1) { 197 | this.advance(); 198 | } 199 | } 200 | 201 | checkCurrentNextChars(chars) { 202 | return this.currentChar === chars[0] && this.peek() === chars[1]; 203 | } 204 | 205 | checkEscapeChar(char) { 206 | return this.currentChar === '\\' && this.peek() === char; 207 | } 208 | 209 | nextToken() { 210 | throw new Error('Cannot call Lexer this way. Did you forget to subclass it?'); 211 | } 212 | } 213 | 214 | class PhraseLexer extends Lexer { 215 | constructor(text) { 216 | super(text); 217 | this.insideRef = false; 218 | } 219 | 220 | _string(terminator) { 221 | let result = ''; 222 | while (this.currentChar !== null) { 223 | if (this.checkEscapeChar(terminator)) { 224 | result += terminator; 225 | this.advance(); 226 | this.advance(); 227 | } else if (this.currentChar === terminator) { 228 | this.advance(); 229 | break; 230 | } else { 231 | result += this.currentChar; 232 | this.advance(); 233 | } 234 | } 235 | return new Token(STRING, result); 236 | } 237 | 238 | _number() { 239 | let result = ''; 240 | while (this.currentChar !== null && DIGITS.indexOf(this.currentChar) !== -1) { 241 | result += this.currentChar; 242 | this.advance(); 243 | } 244 | 245 | if (this.currentChar === '.' && this.peek() !== '.') { 246 | result += this.currentChar; 247 | this.advance(); 248 | 249 | while (this.currentChar !== null && DIGITS.indexOf(this.currentChar) !== -1) { 250 | result += this.currentChar; 251 | this.advance(); 252 | } 253 | return new Token(REAL_CONST, parseFloat(result)); 254 | } else { 255 | return new Token(INTEGER_CONST, parseInt(result)); 256 | } 257 | } 258 | 259 | _key() { 260 | let result = ''; 261 | if (DIGITS.indexOf(this.currentChar) !== -1) { 262 | this.error(this.currentChar); 263 | } 264 | while (this.currentChar !== null && ALPHANUM.indexOf(this.currentChar) !== -1) { 265 | if (this.checkCurrentNextChars('..')) { 266 | break; 267 | } 268 | result += this.currentChar; 269 | this.advance(); 270 | } 271 | return new Token(KEY, result); 272 | } 273 | 274 | _filter() { 275 | let result = ''; 276 | while (this.currentChar !== null && WHITESPACE.indexOf(this.currentChar) !== -1) { 277 | this.skipWhitespace(); 278 | continue; 279 | } 280 | while (this.currentChar !== null && ALPHANUM.indexOf(this.currentChar) !== -1) { 281 | if (this.checkCurrentNextChars('..')) { 282 | break; 283 | } 284 | result += this.currentChar; 285 | this.advance(); 286 | } 287 | return new Token(FILTER, result); 288 | } 289 | 290 | _varg() { 291 | let result = ''; 292 | while (this.currentChar !== null && WHITESPACE.indexOf(this.currentChar) !== -1) { 293 | this.skipWhitespace(); 294 | continue; 295 | } 296 | while (this.currentChar !== null && ALPHANUM.indexOf(this.currentChar) !== -1) { 297 | result += this.currentChar; 298 | this.advance(); 299 | } 300 | return new Token(VAR_GLOBAL, result); 301 | } 302 | 303 | _animRange() { 304 | while (this.currentChar !== null) { 305 | this.skipWhitespace(); 306 | let start, end; 307 | let negate = false; 308 | if (this.currentChar === '-') { 309 | negate = true; 310 | this.advance(); 311 | } 312 | if (DIGITS.indexOf(this.currentChar) !== -1) { 313 | start = this._number().value; 314 | if (negate) { 315 | start = -start; 316 | } 317 | } else { 318 | this.error(this.currentChar); 319 | } 320 | negate = false; 321 | this.skipWhitespace(); 322 | if (this.currentChar !== ',') { 323 | this.error(this.currentChar); 324 | } else { 325 | this.advance(); 326 | } 327 | this.skipWhitespace(); 328 | if (this.currentChar === '-') { 329 | negate = true; 330 | this.advance(); 331 | } 332 | if (DIGITS.indexOf(this.currentChar) !== -1) { 333 | end = this._number().value; 334 | if (negate) { 335 | end = -end; 336 | } 337 | } else { 338 | this.error(this.currentChar); 339 | } 340 | this.skipWhitespace(); 341 | if (this.currentChar !== ']') { 342 | this.error(this.currentChar); 343 | } else { 344 | this.advance(); 345 | } 346 | return new Token(ANIMATION_RANGE, {start, end}); 347 | } 348 | } 349 | 350 | _ref() { 351 | let result = ''; 352 | while (this.currentChar !== null) { 353 | if (WHITESPACE.indexOf(this.currentChar) !== -1) { 354 | this.skipWhitespace(); 355 | continue; 356 | } else if (this.checkCurrentNextChars(VARIABLE_TAG_START)) { 357 | this.error(VARIABLE_TAG_START); 358 | } else if (this.checkCurrentNextChars(VARIABLE_TAG_END)) { 359 | this.advance(); 360 | this.advance(); 361 | this.insideRef = false; 362 | return new Token(REF_END, VARIABLE_TAG_END); 363 | } else if (this.currentChar === '"') { 364 | this.advance(); 365 | return this._string('"'); 366 | } else if (this.currentChar === '\'') { 367 | this.advance(); 368 | return this._string("'"); 369 | } else if (this.checkCurrentNextChars('..')) { 370 | this.advance(); 371 | this.advance(); 372 | return new Token(RANGE, '..'); 373 | } else if (this.currentChar === '[') { 374 | this.advance(); 375 | return this._animRange(); 376 | } else if (this.currentChar === '|') { 377 | this.advance(); 378 | return this._filter(); 379 | } else if (this.currentChar === ':') { 380 | this.advance(); 381 | return this._varg(); 382 | } else if (DIGITS.indexOf(this.currentChar) !== -1) { 383 | return this._number(); 384 | } else if (this.currentChar === ',') { 385 | this.advance(); 386 | return new Token(COMMA, ','); 387 | } else if (this.currentChar === '+') { 388 | this.advance(); 389 | return new Token(PLUS, '+'); 390 | } else if (this.currentChar === '-') { 391 | this.advance(); 392 | return new Token(MINUS, '-'); 393 | } else if (this.currentChar === '*') { 394 | this.advance(); 395 | return new Token(MUL, '*'); 396 | } else if (this.currentChar === '/') { 397 | this.advance(); 398 | return new Token(DIV, '/'); 399 | } else if (this.currentChar === '(') { 400 | this.advance(); 401 | return new Token(LPAREN, '('); 402 | } else if (this.currentChar === ')') { 403 | this.advance(); 404 | return new Token(RPAREN, ')'); 405 | } else if (ALPHANUM.indexOf(this.currentChar) !== -1) { 406 | return this._key(); 407 | } 408 | 409 | result += this.currentChar; 410 | 411 | if (result) { 412 | this.error(result); 413 | } 414 | } 415 | } 416 | 417 | _text() { 418 | if (this.checkCurrentNextChars(VARIABLE_TAG_START)) { 419 | this.advance(); 420 | this.advance(); 421 | this.insideRef = true; 422 | return new Token(REF_START, VARIABLE_TAG_START); 423 | } 424 | let result = ''; 425 | while (this.currentChar !== null) { 426 | if (this.checkCurrentNextChars(VARIABLE_TAG_END)) { 427 | this.error(VARIABLE_TAG_END); 428 | } else if (this.checkCurrentNextChars(VARIABLE_TAG_START)) { 429 | break; 430 | } else if (this.checkEscapeChar('{')) { 431 | result += '{'; 432 | this.advance(); 433 | this.advance(); 434 | } else if (this.checkEscapeChar('}')) { 435 | result += '}'; 436 | this.advance(); 437 | this.advance(); 438 | } else { 439 | result += this.currentChar; 440 | this.advance(); 441 | } 442 | } 443 | return new Token(TEXT, result); 444 | } 445 | 446 | nextToken() { 447 | while (this.currentChar !== null) { 448 | return this.insideRef ? this._ref() : this._text(); 449 | } 450 | return new Token(EOF, null); 451 | } 452 | } 453 | 454 | class DefLexer extends Lexer { 455 | _key() { 456 | let result = ''; 457 | if (DIGITS.indexOf(this.currentChar) !== -1) { 458 | this.error(this.currentChar); 459 | } 460 | while (this.currentChar !== null && ALPHANUM.indexOf(this.currentChar) !== -1) { 461 | if (this.checkCurrentNextChars('..')) { 462 | break; 463 | } 464 | result += this.currentChar; 465 | this.advance(); 466 | } 467 | return new Token(KEY, result); 468 | } 469 | 470 | nextToken() { 471 | let result = ''; 472 | while (this.currentChar !== null) { 473 | if (WHITESPACE.indexOf(this.currentChar) !== -1) { 474 | this.skipWhitespace(); 475 | continue; 476 | } else if (this.currentChar === ':') { 477 | this.advance(); 478 | return new Token(COLON, ':'); 479 | } else if (this.currentChar === ',') { 480 | this.advance(); 481 | return new Token(COMMA, ','); 482 | } else if (this.currentChar === '(') { 483 | this.advance(); 484 | return new Token(LPAREN, '('); 485 | } else if (this.currentChar === ')') { 486 | this.advance(); 487 | return new Token(RPAREN, ')'); 488 | } else if (ALPHANUM.indexOf(this.currentChar) !== -1) { 489 | return this._key(); 490 | } 491 | result += this.currentChar; 492 | 493 | if (result) { 494 | this.error(result); 495 | } 496 | } 497 | return new Token(EOF, null); 498 | } 499 | } 500 | 501 | const NODE_CONCAT = 'Concat'; 502 | const NODE_TEXT = 'Text'; 503 | const NODE_REF = 'Ref'; 504 | const NODE_INTEGER = 'Integer'; 505 | const NODE_REAL = 'Real'; 506 | const NODE_STRING = 'String'; 507 | const NODE_RANGE = 'Range'; 508 | const NODE_KEY = 'Key'; 509 | const NODE_NAMED_KEY = 'NamedKey'; 510 | const NODE_CHAR = 'Char'; 511 | const NODE_FILTER = 'Filter'; 512 | const NODE_ANIMATION_RANGE = 'AnimRange'; 513 | const NODE_UNARY_OP = 'UnaryOp'; 514 | const NODE_BINARY_OP = 'BinaryOp'; 515 | const NODE_NO_OP = 'NoOp'; 516 | 517 | class Node { 518 | constructor(type, data) { 519 | this.type = type; 520 | if (data) { 521 | Object.assign(this, data); 522 | } 523 | } 524 | } 525 | 526 | class Parser { 527 | constructor(lexer, lineno) { 528 | this.lexer = lexer; 529 | this.lineno = lineno; 530 | this.currentToken = this.lexer.nextToken(); 531 | } 532 | 533 | error() { 534 | throw new Error('Cannot call Parser this way. Did you forget to subclass it?'); 535 | } 536 | 537 | consume(tokenType) { 538 | if (this.currentToken.type === tokenType) { 539 | this.currentToken = this.lexer.nextToken(); 540 | } else { 541 | this.error(tokenType); 542 | } 543 | } 544 | 545 | parse() { 546 | throw new Error('Cannot call Parser this way. Did you forget to subclass it?'); 547 | } 548 | } 549 | 550 | class PhraseParser extends Parser { 551 | error(tokenType) { 552 | throw new Error(`Invalid syntax: expected a symbol of type ${tokenType} at position ${this.lexer.pos}, but encountered ${this.currentToken.type} instead.`); 553 | } 554 | 555 | _filters(node) { 556 | while (this.currentToken.type === FILTER) { 557 | if (this.currentToken.value === '') { 558 | throw new Error(`Naming Error. Encountered a filter token (|) at position ${ this.lexer.pos } without a filter name.`); 559 | } 560 | node = new Node(NODE_FILTER, { node, name: this.currentToken.value }); 561 | this.consume(FILTER); 562 | node = this._parameters(node); 563 | } 564 | return node; 565 | } 566 | 567 | _range(node) { 568 | if (this.currentToken.type === RANGE) { 569 | if (node.type === NODE_STRING && node.value.length !== 1) { 570 | throw new Error(`Range Error: Only single character strings can be part of a range. The encountered string '${ node.value }' at position ${ this.lexer.pos } has a length of ${ node.value.length }.`); 571 | } 572 | this.consume(RANGE); 573 | let start = node; 574 | let end = this.factor(false); 575 | if (end.type === NODE_STRING && end.value.length !== 1) { 576 | throw new Error(`Range Error: Only single character strings can be part of a range. The encountered string '${ end.value }' at position ${ this.lexer.pos } has a length of ${ end.value.length }.`); 577 | } 578 | if (start.type === NODE_KEY && start.key.length === 1 && !start.parameters) { 579 | start = new Node(NODE_CHAR, { value: start.key }); 580 | } 581 | if (end.type === NODE_KEY && end.key.length === 1 && !end.parameters) { 582 | end = new Node(NODE_CHAR, { value: end.key }); 583 | } 584 | node = new Node(NODE_RANGE, { start, end }); 585 | node = this._filters(node); 586 | } 587 | return node; 588 | } 589 | 590 | _name(node) { 591 | let token = this.currentToken; 592 | if (this.currentToken.type === VAR_GLOBAL) { 593 | this.consume(VAR_GLOBAL); 594 | node = new Node(NODE_NAMED_KEY, { key: node.key, name: token.value }); 595 | } 596 | return node; 597 | } 598 | 599 | _parameters(node) { 600 | const parameters = []; 601 | if (this.currentToken.type === LPAREN) { 602 | this.consume(LPAREN); 603 | if (this.currentToken.type === RPAREN) { 604 | node.parameters = parameters; 605 | this.consume(RPAREN); 606 | return node; 607 | } 608 | parameters.push(this.expr()); 609 | while (this.currentToken.type === COMMA) { 610 | this.consume(COMMA); 611 | parameters.push(this.expr()); 612 | } 613 | if (this.currentToken.type === RPAREN) { 614 | this.consume(RPAREN); 615 | node.parameters = parameters; 616 | return node; 617 | } else { 618 | this.error(RPAREN); 619 | } 620 | } 621 | return node; 622 | } 623 | 624 | factor(parseFiltersRange = true) { 625 | const token = this.currentToken; 626 | let node; 627 | if (token.type === PLUS) { 628 | this.consume(PLUS); 629 | node = new Node(NODE_UNARY_OP, { op: token.type, expression: this.factor(false) }); 630 | } else if (token.type === MINUS) { 631 | this.consume(MINUS); 632 | node = new Node(NODE_UNARY_OP, { op: token.type, expression: this.factor(false) }); 633 | } else if (token.type === INTEGER_CONST) { 634 | this.consume(INTEGER_CONST); 635 | node = new Node(NODE_INTEGER, { value: token.value }); 636 | } else if (token.type === REAL_CONST) { 637 | this.consume(REAL_CONST); 638 | node = new Node(NODE_REAL, { value: token.value }); 639 | } else if (token.type === ANIMATION_RANGE) { 640 | this.consume(ANIMATION_RANGE); 641 | node = new Node(NODE_ANIMATION_RANGE, { start: token.value.start, end: token.value.end }); 642 | } else if (token.type === STRING) { 643 | this.consume(STRING); 644 | node = new Node(NODE_STRING, { value: token.value }); 645 | } else if (token.type === KEY) { 646 | this.consume(KEY); 647 | if (this.currentToken.type === KEY) { 648 | throw new Error(`Invalid syntax at position ${this.lexer.pos}: Spaces are not allowed as part of identifiers. You could write '${token.value}_${this.currentToken.value}' instead.`); 649 | } 650 | node = new Node(NODE_KEY, { key: token.value }); 651 | node = this._name(node); 652 | node = this._parameters(node); 653 | } else if (token.type === LPAREN) { 654 | this.consume(LPAREN); 655 | try { 656 | node = this.expr(); 657 | } catch (e) { 658 | throw new Error(`Error. Empty expression at position ${this.lexer.pos}.`); 659 | } 660 | this.consume(RPAREN); 661 | } else { 662 | throw new Error(`Invalid syntax: expected a symbol (an integer, float, string, ...) at position ${this.lexer.pos}, but encountered ${this.currentToken.type} instead.`); 663 | } 664 | if (parseFiltersRange) { 665 | node = this._filters(node); 666 | node = this._range(node); 667 | } 668 | return node; 669 | } 670 | 671 | term() { 672 | let node = this.factor(); 673 | while (this.currentToken.type === MUL || this.currentToken.type === DIV) { 674 | let token = this.currentToken; 675 | if (token.type === MUL) { 676 | this.consume(MUL); 677 | } else if (token.type === DIV) { 678 | this.consume(DIV); 679 | } 680 | node = new Node(NODE_BINARY_OP, { left: node, op: token.type, right: this.factor() }); 681 | } 682 | return node; 683 | } 684 | 685 | expr() { 686 | if (this.currentToken.type === REF_END) { 687 | return new Node(NODE_NO_OP); 688 | } else if (this.currentToken.type === RPAREN) { 689 | throw new Error(`Invalid syntax: Encountered ) symbol at position ${this.lexer.pos} but no ( was seen.`); 690 | } 691 | let node = this.term(); 692 | while (this.currentToken.type === PLUS || this.currentToken.type === MINUS) { 693 | let token = this.currentToken; 694 | if (token.type === PLUS) { 695 | this.consume(PLUS); 696 | } else if (token.type === MINUS) { 697 | this.consume(MINUS); 698 | } 699 | node = new Node(NODE_BINARY_OP, { left: node, op: token.type, right: this.term() }); 700 | } 701 | return node; 702 | } 703 | 704 | ref() { 705 | let node = this.expr(); 706 | if (this.currentToken.type !== REF_END) { 707 | throw new Error(`Invalid syntax: expected end of reference at position ${this.lexer.pos}, but encountered ${this.currentToken.type} instead.`); 708 | } 709 | return new Node(NODE_REF, { node }); 710 | } 711 | 712 | part() { 713 | const token = this.currentToken; 714 | let node; 715 | if (token.type === TEXT) { 716 | this.consume(TEXT); 717 | return new Node(NODE_TEXT, { text: token.value }); 718 | } else if (token.type === REF_START) { 719 | this.consume(REF_START); 720 | node = this.ref(); 721 | this.consume(REF_END); 722 | return node; 723 | } 724 | } 725 | 726 | phrase() { 727 | let node = this.part(); 728 | while (this.currentToken.type === TEXT || this.currentToken.type === REF_START) { 729 | node = new Node(NODE_CONCAT, { left: node, right: this.part() }); 730 | } 731 | return node; 732 | } 733 | 734 | parse() { 735 | try { 736 | const node = this.phrase(); 737 | if (this.currentToken.type !== EOF) { 738 | this.error(EOF); 739 | } 740 | return node; 741 | } catch (e) { 742 | throw new Error(`Line ${this.lineno}: ${e.message}`); 743 | } 744 | } 745 | } 746 | 747 | class DefParser extends Parser { 748 | error(tokenType) { 749 | throw new Error(`Invalid syntax: expected a symbol of type ${tokenType} at position ${this.lexer.pos}, but encountered ${this.currentToken.type} instead.`); 750 | } 751 | 752 | _key() { 753 | let key = this.currentToken.value; 754 | this.consume(KEY); 755 | if (this.currentToken.type === KEY) { 756 | throw new Error(`Invalid syntax at position ${this.lexer.pos}: Spaces are not allowed as part of identifiers. You could write '${key}_${this.currentToken.value}' instead.`); 757 | } 758 | return key; 759 | } 760 | 761 | _parameters() { 762 | const parameters = []; 763 | if (this.currentToken.type === LPAREN) { 764 | this.consume(LPAREN); 765 | if (this.currentToken.type === RPAREN) { 766 | this.consume(RPAREN); 767 | return parameters; 768 | } 769 | parameters.push(this._key()); 770 | while (this.currentToken.type === COMMA) { 771 | this.consume(COMMA); 772 | parameters.push(this._key()); 773 | } 774 | if (this.currentToken.type === RPAREN) { 775 | this.consume(RPAREN); 776 | return parameters; 777 | } else { 778 | this.error(RPAREN); 779 | } 780 | } 781 | return parameters; 782 | } 783 | 784 | parse() { 785 | try { 786 | const key = this._key(); 787 | const parameters = this._parameters(); 788 | this.consume(COLON); 789 | if (this.currentToken.type !== EOF) { 790 | this.error(EOF); 791 | } 792 | if (parameters.length === 0) { 793 | return {key}; 794 | } else { 795 | return {key, parameters}; 796 | } 797 | } catch (e) { 798 | throw new Error(`Line ${this.lineno}: ${e.message}`); 799 | } 800 | } 801 | } 802 | 803 | class Interpreter { 804 | constructor(data) { 805 | if (data) { 806 | Object.assign(this, data); 807 | } 808 | this.globalScope = {}; 809 | } 810 | 811 | async visit(node) { 812 | const methodName = 'visit' + node.type; 813 | if (this[methodName]) { 814 | return await this[methodName](node); 815 | } 816 | this.genericVisit(node); 817 | } 818 | 819 | genericVisit(node) { 820 | throw new Error(`No visit${node.type} method available for node ${node}.`); 821 | } 822 | 823 | visitNoOp(node) { 824 | return ''; 825 | } 826 | 827 | async visitUnaryOp(node) { 828 | const op = node.op; 829 | if (op === PLUS) { 830 | return + (await this.visit(node.expression)); 831 | } else if (op === MINUS) { 832 | return - (await this.visit(node.expression)); 833 | } 834 | } 835 | 836 | async visitBinaryOp(node) { 837 | const op = node.op; 838 | if (op === PLUS) { 839 | return await this.visit(node.left) + await this.visit(node.right); 840 | } else if (op === MINUS) { 841 | return await this.visit(node.left) - await this.visit(node.right); 842 | } else if (op === MUL) { 843 | return await this.visit(node.left) * await this.visit(node.right); 844 | } else if (op === DIV) { 845 | return await this.visit(node.left) / await this.visit(node.right); 846 | } 847 | } 848 | 849 | async visitConcat(node) { 850 | return await this.visit(node.left) + await this.visit(node.right); 851 | } 852 | 853 | visitText(node) { 854 | return node.text; 855 | } 856 | 857 | async visitRef(node) { 858 | return String(await this.visit(node.node)); 859 | } 860 | 861 | visitString(node) { 862 | return node.value; 863 | } 864 | 865 | visitInteger(node) { 866 | return node.value; 867 | } 868 | 869 | visitReal(node) { 870 | return node.value; 871 | } 872 | 873 | visitChar(node) { 874 | return node.value; 875 | } 876 | 877 | async visitRange(node) { 878 | let start, end; 879 | let startError, endError; 880 | if (node.start.type === NODE_CHAR) { 881 | start = node.start.value; 882 | if (this.localMemory[start] !== undefined) { 883 | start = this.localMemory[start]; 884 | } else if (this.phraseBook[start] !== undefined) { 885 | start = await this.visitKey(new Node(NODE_KEY, {key: start})); 886 | } 887 | } else { 888 | start = await this.visit(node.start); 889 | } 890 | if (typeof start === 'string' && String(parseFloat(start)) === start) { 891 | start = parseFloat(start); 892 | } 893 | if (node.end.type === NODE_CHAR) { 894 | end = node.end.value; 895 | if (this.localMemory[end] !== undefined) { 896 | end = this.localMemory[end]; 897 | } else if (this.phraseBook[end] !== undefined) { 898 | end = await this.visitKey(new Node(NODE_KEY, {key: end})); 899 | } 900 | } else { 901 | end = await this.visit(node.end); 902 | } 903 | if (typeof end === 'string' && end.length === 1 && String(parseFloat(end)) === end) { 904 | end = parseFloat(end); 905 | } 906 | if (typeof start === 'number' && typeof end === 'number') { 907 | return Math.floor(rand(start, end)); 908 | } 909 | let min, max, charCode; 910 | if (typeof start === 'string' && start.length === 1 && typeof end === 'string' && end.length === 1) { 911 | return randomChar(start, end); 912 | } else if (typeof start === 'string' && start.length === 1 && typeof end === 'number' && String(end).length === 1) { 913 | return randomChar(start, String(end)); 914 | } else if (typeof start === 'number' && String(start).length === 1 && typeof end === 'string' && end.length === 1) { 915 | return randomChar(String(start), end); 916 | } else { 917 | if (typeof start === 'string') { 918 | start = `"${start}"`; 919 | } 920 | if (typeof end === 'string') { 921 | end = `"${end}"`; 922 | } 923 | throw new Error(`Range Error: ${start}..${end}`); 924 | } 925 | } 926 | 927 | async visitKey(node, searchLocal=true) { 928 | if (searchLocal && this.localMemory[node.key] !== undefined) { 929 | return this.localMemory[node.key]; 930 | } 931 | let key, phraseBook, globalMemory; 932 | if (this.phraseBook['%imports'][node.key] !== undefined) { 933 | phraseBook = this.phraseBook['%imports'][node.key]; 934 | key = 'root'; 935 | globalMemory = {}; 936 | } else { 937 | phraseBook = this.phraseBook; 938 | key = node.key; 939 | globalMemory = this.globalMemory; 940 | } 941 | const phrase = lookupPhrase(phraseBook, key); 942 | const localMemory = {}; 943 | const parameters = phraseBook[key].parameters; 944 | if (parameters) { 945 | for (let i = 0; i < parameters.length; i += 1) { 946 | let name = parameters[i]; 947 | if (node.parameters && node.parameters[i]) { 948 | localMemory[name] = await this.visit(node.parameters[i]); 949 | } else { 950 | localMemory[name] = ''; 951 | } 952 | } 953 | } 954 | return evalPhrase(phraseBook, phrase, globalMemory, localMemory, this.t, this.level + 1, this.startTime); 955 | } 956 | 957 | async visitNamedKey(node) { 958 | const name = `${node.key}:${node.name}`; 959 | if (!this.globalMemory[name]) { 960 | this.globalMemory[name] = await this.visitKey(node); 961 | } 962 | return this.globalMemory[name]; 963 | } 964 | 965 | async visitRepeatFilter(node) { 966 | if (!node.parameters || node.parameters.length === 0) { 967 | throw new Error('Repeat filter takes a positive integer argument.'); 968 | } 969 | const times = parseInt(await this.visit(node.parameters[0])); 970 | if (isNaN(times)) { 971 | throw new Error('Repeat filter takes a positive integer argument.'); 972 | } else if (times <= 0) { 973 | throw new Error(`Repeat filter takes one positive integer argument, not ${times}`); 974 | } 975 | let s = ''; 976 | for (let i = 0; i < times; i += 1) { 977 | s += await this.visit(node.node); 978 | } 979 | return s; 980 | } 981 | 982 | jsEvaluator(source) { 983 | const evaluator = new Function(source + 'return function (_fn, args) { return eval(_fn).apply(null, args) };'); 984 | return evaluator(); 985 | } 986 | 987 | async visitJSFilter(node) { 988 | if (!node.parameters || node.parameters.length === 0) { 989 | throw new Error('JS filter takes at least one argument.'); 990 | } 991 | const source = await this.visit(node.node); 992 | let evaluator; 993 | if (node.node.type === NODE_NAMED_KEY) { 994 | const name = `${node.key}:${node.name}:__evaluator`; 995 | if (!this.globalMemory[name]) { 996 | this.globalMemory[name] = this.jsEvaluator(source); 997 | } 998 | evaluator = this.globalMemory[name]; 999 | } else { 1000 | evaluator = this.jsEvaluator(source); 1001 | } 1002 | const fnName = await this.visit(node.parameters[0]); 1003 | let parameters = []; 1004 | if (node.parameters.length > 1) { 1005 | for (let i = 1; i < node.parameters.length; i += 1) { 1006 | parameters.push(await this.visit(node.parameters[i])); 1007 | } 1008 | } 1009 | return evaluator(fnName, parameters); 1010 | } 1011 | 1012 | async visitFilter(node) { 1013 | const f = node.name; 1014 | if (f === 'repeat') { 1015 | return this.visitRepeatFilter(node); 1016 | } else if (f === 'js') { 1017 | return this.visitJSFilter(node); 1018 | } 1019 | const v = await this.visit(node.node); 1020 | if (f === 'upper') { 1021 | return String(v).toUpperCase(); 1022 | } else if (f === 'lower') { 1023 | return String(v).toLowerCase(); 1024 | } else if (f === 'title') { 1025 | return String(v).toTitleCase(); 1026 | } else if (f === 'sentence') { 1027 | return String(v).substring(0, 1).toUpperCase() + String(v).substring(1); 1028 | } else if (f === 'str') { 1029 | return String(v); 1030 | } else if (f === 'int') { 1031 | let result = parseInt(v); 1032 | return isNaN(result) ? 0 : result; 1033 | } else if (f === 'float') { 1034 | let result = parseFloat(v); 1035 | return isNaN(result) ? 0 : result; 1036 | } else { 1037 | throw new Error(`Unknown filter "${f}".`); 1038 | } 1039 | } 1040 | 1041 | visitAnimRange(node) { 1042 | const type = this.phraseBook['%preamble'].animation || 'bounce'; 1043 | return lerp(node.start, node.end, this.t, type); 1044 | } 1045 | 1046 | interpret() { 1047 | try { 1048 | return this.visit(this.phrase.tree); 1049 | } catch (e) { 1050 | throw new Error(`Line ${this.phrase.lineno}: ${e.message}`) 1051 | } 1052 | } 1053 | } 1054 | 1055 | function parsePhrase(phrase, lineno) { 1056 | const lexer = new PhraseLexer(phrase); 1057 | const parser = new PhraseParser(lexer, lineno); 1058 | const tree = parser.parse(); 1059 | return tree; 1060 | } 1061 | 1062 | function maxLevel(phraseBook) { 1063 | const depth = phraseBook['%preamble'].depth; 1064 | if (depth !== undefined) { return depth; } 1065 | return MAX_LEVEL; 1066 | } 1067 | 1068 | function evalPhrase(phraseBook, phrase, globalMemory, localMemory, t=0.0, level=0, startTime=0) { 1069 | if (level > maxLevel(phraseBook)) return ''; 1070 | if (TIMEOUT && startTime > 0 && Date.now() - startTime > TIMEOUT_MILLIS) { 1071 | throw new Error('Evaluation timed out. Do you have a recursive function?'); 1072 | } 1073 | let interpreter = new Interpreter({ phraseBook, phrase, globalMemory, localMemory, t, level, startTime }); 1074 | return interpreter.interpret(); 1075 | } 1076 | 1077 | function lookupPhrase(phraseBook, key) { 1078 | const v = phraseBook[key]; 1079 | if (v === undefined) { 1080 | throw new Error(`Could not find phrase with key "${key}".`); 1081 | } 1082 | console.assert(Array.isArray(v)); 1083 | return choice(v); 1084 | } 1085 | 1086 | function parsePreamble(preamble, key, value, lineno) { 1087 | if (PREAMBLE_KEYS.indexOf(key) === -1) { 1088 | throw new Error(`Line ${lineno}: unknown '${key}' property in preamble.`); 1089 | } 1090 | if (value === undefined) { 1091 | throw new Error(`Line ${lineno}: no value given for '${key}' property.`); 1092 | } 1093 | value = value.trim(); 1094 | if (key === 'depth') { 1095 | if (!value.match(POS_INTEGER_RE)) { 1096 | throw new Error(`Line ${lineno}: expecting integer value for 'depth' property, not '${value}'.`); 1097 | } else { 1098 | preamble[key] = parseInt(value); 1099 | } 1100 | } else if (key === 'duration') { 1101 | let m = value.match(DURATION_RE); 1102 | if (!m || !m[3]) { 1103 | throw new Error(`Line ${lineno}: expecting seconds (e.g. 1s) or milliseconds (e.g. 1000ms) for 'duration' property, not '${value}'.`); 1104 | } else { 1105 | if (m[3] === 'ms') { 1106 | preamble[key] = parseFloat(m[1]) / 1000; 1107 | } else { 1108 | preamble[key] = parseFloat(m[1]); 1109 | } 1110 | } 1111 | } else if (key === 'animation') { 1112 | if (ANIMATION_TYPES.indexOf(value) === -1) { 1113 | throw new Error(`Line ${lineno}: expecting one of bounce/linear/once for 'animation' property, not '${value}'.`); 1114 | } else { 1115 | preamble[key] = value; 1116 | } 1117 | } else if (key === 'script') { 1118 | if (!value.endsWith('.js')) throw new Error(`Line ${lineno}: expecting script preamble to end with .js.`); 1119 | const currentScripts = Array.from(document.head.getElementsByTagName('script')); 1120 | if (currentScripts.some(tag => tag.src === value)) return; 1121 | const tag = document.createElement('script'); 1122 | tag.setAttribute('src', value); 1123 | document.head.appendChild(tag); 1124 | } 1125 | } 1126 | 1127 | const importedSketches = {}; 1128 | 1129 | async function parsePhraseBook(s, loadSketch) { 1130 | const importSketches = []; 1131 | const preamble = {}; 1132 | const phrases = []; 1133 | let currentPhrase; 1134 | const lines = s.split('\n'); 1135 | for (let i = 0; i < lines.length; i++) { 1136 | const line = lines[i]; 1137 | const trimmedLine = line.trim(); 1138 | if (line.startsWith(' ')) { 1139 | // Phrases continue with two spaces. 1140 | // This comes first because some of the next rules trim the spaces, which we don't want here. 1141 | if (!currentPhrase) { 1142 | throw new Error(`Line ${ i + 1 }: continuation line without a starting block.`); 1143 | } 1144 | const lastIndex = currentPhrase.values.length - 1; 1145 | if (lastIndex < 0) { 1146 | throw new Error(`Line ${ i + 1 }: continuation line without a previous line.`); 1147 | } 1148 | const lastPhrase = currentPhrase.values[lastIndex]; 1149 | console.assert(typeof lastPhrase === 'string'); 1150 | currentPhrase.values[lastIndex] = lastPhrase + line.substring(2); 1151 | } else if (trimmedLine[0] === '#') { 1152 | // Ignore comments 1153 | currentPhrase = undefined; 1154 | continue; 1155 | } else if (trimmedLine.length === 0) { 1156 | // Ignore empty lines 1157 | currentPhrase = undefined; 1158 | continue; 1159 | } else if (line.startsWith('%')) { 1160 | // Preamble 1161 | currentPhrase = undefined; 1162 | let m, l = trimmedLine.slice(1).trim(); 1163 | if (l.startsWith('import')) { 1164 | m = l.match(IMPORT_RE); 1165 | if (m) { 1166 | importSketches.push({name: m[1], alias: m[2], line: i + 1}); 1167 | continue; 1168 | } else { 1169 | throw new Error(`Line ${ i + 1 }: Error in import statement.`); 1170 | } 1171 | } 1172 | m = line.slice(1).match(PREAMBLE_RE); 1173 | if (m) { 1174 | parsePreamble(preamble, m[1], m[2], i + 1); 1175 | } 1176 | else if (trimmedLine.length !== 0) { 1177 | throw new Error(`Line ${ i + 1}: expecting '% value: property' for the preamble.`); 1178 | } 1179 | continue; 1180 | } else if (line.startsWith('- ')) { 1181 | // Phrases are prefixed with '-'. 1182 | if (!currentPhrase) { 1183 | throw new Error(`Line ${ i + 1 }: line without a key.`); 1184 | } 1185 | currentPhrase.values.push(line.substring(2)); 1186 | currentPhrase.lines.push(i + 1); 1187 | } else if (trimmedLine.endsWith(':')) { 1188 | // Keys end with ":" 1189 | let parser = new DefParser(new DefLexer(trimmedLine), i + 1); 1190 | currentPhrase = parser.parse(); 1191 | currentPhrase.values = []; 1192 | currentPhrase.lines = []; 1193 | currentPhrase.lineno = i + 1; 1194 | phrases.push(currentPhrase); 1195 | } else { 1196 | throw new Error(`Line ${ i + 1 }: do not know what to do with line "${line}".`); 1197 | } 1198 | } 1199 | 1200 | const imports = {}; 1201 | for (let i = 0; i < importSketches.length; i += 1) { 1202 | let o = importSketches[i]; 1203 | let sketch; 1204 | if (importedSketches[o.name]) { 1205 | sketch = importedSketches[o.name]; 1206 | } else { 1207 | sketch = await loadSketch(o.name); 1208 | if (sketch === null || sketch.source === undefined) { 1209 | throw new Error(`Line ${ o.line }: Could not import sketch named "${o.name}".`) 1210 | } 1211 | importedSketches[o.name] = sketch; 1212 | } 1213 | let pb = await parsePhraseBook(sketch.source, loadSketch); 1214 | imports[o.alias] = pb; 1215 | } 1216 | const phraseBook = {}; 1217 | for (let phrase of phrases) { 1218 | phraseBook[phrase.key] = phrase.values.map((text, index) => ({text, tree: parsePhrase(text, phrase.lines[index]), lineno: phrase.lines[index]})); 1219 | phraseBook[phrase.key].lineno = phrase.lineno; 1220 | if (phrase.parameters) { 1221 | phraseBook[phrase.key].parameters = phrase.parameters; 1222 | } 1223 | } 1224 | phraseBook['%preamble'] = preamble; 1225 | phraseBook['%imports'] = imports; 1226 | return phraseBook; 1227 | } 1228 | 1229 | function generateString(phraseBook, rootKey = 'root', globalMemory = {}, seed = 1234, t = 0.0) { 1230 | Math.seedrandom(seed); 1231 | const startTime = Date.now(); 1232 | return evalPhrase(phraseBook, lookupPhrase(phraseBook, rootKey), globalMemory, {}, t, 0, startTime); 1233 | } -------------------------------------------------------------------------------- /js/editor.js: -------------------------------------------------------------------------------- 1 | const { h, render, Component } = preact; 2 | const { route, Router, Link } = preactRouter; 3 | 4 | const BASE_REST_URL = config.databaseURL; 5 | const SKETCH_REST_URL = BASE_REST_URL + '/sketch'; 6 | 7 | function debounce(func, wait) { 8 | let timeout; 9 | return function() { 10 | const context = this; 11 | const args = arguments; 12 | const later = function() { 13 | timeout = null; 14 | func.apply(context, args); 15 | }; 16 | clearTimeout(timeout); 17 | timeout = setTimeout(later, wait); 18 | }; 19 | } 20 | 21 | function escape(html, encode) { 22 | return html 23 | .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') 24 | .replace(//g, '>') 26 | .replace(/"/g, '"') 27 | .replace(/'/g, '''); 28 | } 29 | 30 | const gMarkedRenderer = new marked.Renderer(); 31 | gMarkedRenderer.code = function(code, lang) { 32 | let html = '
' + escape(code) + '
'; 33 | if (lang === 'seed') { 34 | html = `
${ html }
`; 35 | } 36 | return html; 37 | } 38 | 39 | const INITIAL_TEXT = `root: 40 | - Dear {{ giver }}, thank you for the {{ object }}. 41 | - Hey {{ giver }}, thanks for the {{ object }}! 42 | 43 | giver: 44 | - Aunt Emma 45 | - Dave and Edna 46 | - Uncle Bob 47 | 48 | object: 49 | - purple vase 50 | - golden retriever 51 | - dishwasher 52 | `; 53 | 54 | const GALLERY = [ 55 | '-L4uJCV9VMZPOCBJ6ysM', 56 | '-L0jT5zaERgBPaf3P6LP', 57 | '-L4uLB99JzFHMPcVoiTm', 58 | '-L4uLkY2Yh6GE9_nE6xn', 59 | '-L4uN8VinvdmaAJqMNHJ', 60 | '-L4uMMohcfHFX_pBOr6l', 61 | '-L0jGl9IhooqRuTF9wxS', 62 | '-L4uNdoRFf9FHpsDA1mc', 63 | '-L4uNyhbwpSeFdB0_2J4', 64 | '-L0kGLkGKVe9Iuid9jzC', 65 | '-L4uOhQIyny6CWcP9yvX', 66 | '-L4uP84R9XYlbOomuPl0', 67 | '-L4uPRjyEV237kNmUX5v', 68 | '-L0l7fV1tlR0Gobb64AM', 69 | '-L0pQ_yU-SGVmDoRMfsF', 70 | '-L0tTX3tlVpqHX6Umym4', 71 | '-L4uQH3RqD3QOSbxHhNE', 72 | '-L42j_5BckaAOv8BICMj', 73 | ]; 74 | 75 | class SeedPicker extends Component { 76 | onInput(e) { 77 | const text = e.target.value; 78 | this.props.onSetSeed(text); 79 | } 80 | 81 | render(props) { 82 | return h('div', {class: 'seed-picker'}, 83 | h('span', {class: 'button seed-picker__button seed-picker__prev', onClick: props.onPrevSeed}, '<'), 84 | h('span', {class: 'seed-picker__value'}, 85 | h('input', {class: 'seed-picker__input', type: 'text', value: props.seed, onInput: this.onInput.bind(this)}) 86 | ), 87 | h('span', {class: 'button seed-picker__button seed-picker__next', onClick: props.onNextSeed}, '>') 88 | ) 89 | } 90 | } 91 | 92 | class Header extends Component { 93 | checkUnsaved(e) { 94 | if (this.props.unsaved && !confirm('You have unsaved changes.')) { 95 | e.stopPropagation(); 96 | e.preventDefault(); 97 | } 98 | } 99 | 100 | render(props) { 101 | return h('header', {class: 'header'}, 102 | h('a', {class: 'header__logo', href: '/', onClick: this.checkUnsaved.bind(this) }, 'Seed'), 103 | h('nav', {class: 'header__nav'}, props.children) 104 | ); 105 | } 106 | } 107 | 108 | class Home extends Component { 109 | render(props, state) { 110 | const thumbs = GALLERY.map(id => this.renderThumb(id)); 111 | return h('div', {class: 'app'}, 112 | h(Header, {}, 113 | h('a', {class: 'button', href: '/sketch'}, 'New Sketch') 114 | ), 115 | h('div', {class: 'page'}, 116 | h('section', { class: 'intro' }, 117 | h('div', { class: 'intro__inner' }, 118 | h('p', { class: 'intro__large' }, 119 | 'Seed generates procedural content. Create generated text, shapes or images using web standards.' 120 | ), 121 | h('div', { class: 'intro__cta' }, 122 | h('a', { class: 'button primary', href:'/sketch' }, 'Get Started'), 123 | h('a', { class: 'button', href:'/docs' }, 'Documentation') 124 | ) 125 | ) 126 | ), 127 | h('section', { class: 'gallery' }, 128 | h('div', {class: 'gallery__inner'}, 129 | thumbs 130 | ) 131 | ) 132 | ) 133 | ); 134 | } 135 | 136 | renderThumb(id) { 137 | return h('div', {class: 'gallery__thumb'}, 138 | h('a', {class: 'gallery__link', href: `/sketch/${id}`}, 139 | h('img', {class: 'gallery__img', src: `/gallery/${id}.jpg`}) 140 | ) 141 | ); 142 | } 143 | } 144 | 145 | async function loadSketch(url) { 146 | let getRequest = new Request(`${SKETCH_REST_URL}/${url}.json`); 147 | const res = await fetch(getRequest, { method: 'GET' }); 148 | const json = await res.json(); 149 | return json; 150 | } 151 | 152 | class Source extends Component { 153 | componentDidMount() { 154 | const options = { 155 | value: this.props.source, 156 | indentUnit: 2, 157 | lineNumbers: true, 158 | }; 159 | this.codeMirror = CodeMirror.fromTextArea(this.textAreaRef, options); 160 | this.codeMirror.on('change', this.onChanged.bind(this)); 161 | } 162 | 163 | componentWillUnmount() { 164 | if (this.codeMirror) { 165 | this.codeMirror.toTextArea(); 166 | } 167 | } 168 | 169 | componentWillReceiveProps(nextProps) { 170 | if (!this.codeMirror) return; 171 | if (this.codeMirror.getValue() !== nextProps.source) { 172 | this.codeMirror.setValue(nextProps.source); 173 | } 174 | if (nextProps.importError) { 175 | this.codeMirror.options.readOnly = true; 176 | } 177 | } 178 | 179 | onChanged(doc, change) { 180 | if (change.origin === 'setValue') return; 181 | this.props.onSourceChanged(doc.getValue()); 182 | } 183 | 184 | onInput(e) { 185 | const source = e.target.value; 186 | this.props.onSourceChanged(source); 187 | } 188 | 189 | render(props) { 190 | return h('div', { className: 'editor__area' }, 191 | h('textarea', { ref: ref => (this.textAreaRef = ref), className: 'editor__source', value: this.props.source, onInput: this.onInput.bind(this), readonly: this.props.loading }), 192 | ); 193 | } 194 | } 195 | 196 | class Editor extends Component { 197 | constructor(props) { 198 | super(props); 199 | let state = { debug: true, debugOutput: '', result: '', seed: randomTextSeed(), playing: false, frame: 0 }; 200 | if (!props.id) { 201 | state.source = INITIAL_TEXT; 202 | } else { 203 | state.source = ''; 204 | state.loading = true; 205 | } 206 | this.state = state; 207 | } 208 | 209 | async generate(parse=true) { 210 | try { 211 | if (parse) { 212 | this.phraseBook = await parsePhraseBook(this.state.source, loadSketch); 213 | } 214 | const result = await generateString(this.phraseBook, 'root', {}, this.state.seed); 215 | this.setState({ result: result, debugOutput: '' }); 216 | } catch (e) { 217 | this.setState({ debugOutput: e.message }); 218 | } 219 | } 220 | 221 | async componentDidMount() { 222 | document.querySelector('html').classList.add('fullscreen'); 223 | let localSource; 224 | if (!this.props.id) { 225 | if (this.props.onSourceChanged) { 226 | localSource = window.localStorage.getItem('empty'); 227 | if (localSource !== null && localSource !== undefined) { 228 | this.props.onSourceChanged(localSource, true, true); 229 | this.setState({ source: localSource }); 230 | this.props.setRemoteSource(INITIAL_TEXT); 231 | } else { 232 | this.props.onSourceChanged(INITIAL_TEXT, true); 233 | } 234 | } 235 | if (this.props.onSeedChanged) { 236 | this.props.onSeedChanged(this.state.seed, true); 237 | } 238 | this.generate(); 239 | } else { 240 | const json = await loadSketch(this.props.id); 241 | if (json === null) { 242 | const err = `Error: Could not import sketch named "${this.props.id}".`; 243 | this.setState({ loading: false, debugOutput: err, source: err }); 244 | if (this.props.onImportError) { 245 | this.props.onImportError(); 246 | } 247 | return; 248 | } 249 | const sketch = Object.assign({ key: this.props.id }, json); 250 | let newState = { loading: false, source: sketch.source }; 251 | localSource = window.localStorage.getItem(this.props.id); 252 | let remoteSource; 253 | if (localSource !== null && localSource !== undefined && localSource !== sketch.source) { 254 | remoteSource = sketch.source; 255 | newState.source = localSource; 256 | } 257 | const urlSeed = getURLParameter('seed'); 258 | if (urlSeed) { 259 | newState.seed = urlSeed; 260 | } else if (sketch.seed) { 261 | newState.seed = sketch.seed; 262 | } else { 263 | newState.seed = this.state.seed; 264 | } 265 | 266 | this.setState(newState); 267 | if (remoteSource !== undefined) { 268 | this.props.setRemoteSource(remoteSource); 269 | } 270 | if (this.props.onSourceChanged) { 271 | this.props.onSourceChanged(newState.source, true, localSource !== null && localSource !== undefined); 272 | } 273 | if (this.props.onSeedChanged) { 274 | this.props.onSeedChanged(newState.seed, true); 275 | } 276 | this.generate(); 277 | } 278 | } 279 | 280 | componentWillUnmount() { 281 | document.querySelector('html').classList.remove('fullscreen'); 282 | } 283 | 284 | onSourceChanged(source) { 285 | this.setState({ source }); 286 | if (this.props.onSourceChanged) { 287 | this.props.onSourceChanged(source); 288 | } 289 | this.generate(); 290 | } 291 | 292 | onSetSeed(seed) { 293 | this.setState({seed}); 294 | if (this.props.onSeedChanged) { 295 | this.props.onSeedChanged(seed); 296 | } 297 | this.generate(false); 298 | } 299 | 300 | onGenerate() { 301 | let seed = nextTextSeed(this.state.seed); 302 | this.onSetSeed(seed); 303 | } 304 | 305 | onNextSeed() { 306 | let seed = nextTextSeed(this.state.seed); 307 | this.onSetSeed(seed); 308 | } 309 | 310 | onPrevSeed() { 311 | let seed = prevTextSeed(this.state.seed); 312 | this.onSetSeed(seed); 313 | } 314 | 315 | onTogglePlay() { 316 | if (!this.state.playing) { 317 | this.setState({playing: true, frame: 0}); 318 | this.startTime = Date.now(); 319 | requestAnimationFrame(this.onDoFrame.bind(this)) 320 | } else { 321 | this.setState({playing: false, frame: 0}); 322 | } 323 | } 324 | 325 | durationSeconds() { 326 | const duration = this.phraseBook['%preamble'].duration; 327 | if (duration !== undefined) { return duration; } 328 | return 2.0; 329 | } 330 | 331 | animationType(){ 332 | const type = this.phraseBook['%preamble'].animation; 333 | if (type !== undefined) { return type; } 334 | return 'bounce'; 335 | } 336 | 337 | async onDoFrame() { 338 | if (this.state.playing) { 339 | const animationType = this.animationType(); 340 | 341 | try { 342 | const elapsedSeconds = (Date.now() - this.startTime) / 1000.0; 343 | const durationSeconds = this.durationSeconds(); 344 | const t = (elapsedSeconds / durationSeconds) % 1.0; 345 | 346 | if (animationType === 'once' && durationSeconds <= elapsedSeconds) { 347 | this.setState({ frame: 0, playing: false }); 348 | } else { 349 | const result = await generateString(this.phraseBook, 'root', {}, this.state.seed, t); 350 | this.setState({ frame: this.state.frame + 1, result: result, debugOutput: '' }); 351 | window.requestAnimationFrame(this.onDoFrame.bind(this)); 352 | } 353 | } catch (e) { 354 | this.setState({ frame: 0, playing: false, debugOutput: e.message }); 355 | } 356 | } 357 | } 358 | 359 | restoreRemoteVersion() { 360 | if (confirm('Are you sure you want to restore to the original version? This will discard your changes.')) { 361 | this.setState({ source: this.props.remoteSource }); 362 | this.props.setRemoteSource(undefined); 363 | this.props.onSourceChanged(this.props.remoteSource, true); 364 | window.localStorage.removeItem(this.props.id || 'empty'); 365 | this.generate(); 366 | } 367 | } 368 | 369 | render(props, state) { 370 | const debugView = h('div', { className: 'editor__debug' }, this.state.debugOutput); 371 | const source = state.loading ? 'Loading...' : state.source; 372 | let localVersionDiv; 373 | if (this.props.remoteSource) { 374 | localVersionDiv = h('div', { className: 'localversion' }, 375 | 'You\'ve previously made some changes to this sketch. ', 376 | h('a', {class: 'underline', href: '#', onClick: this.restoreRemoteVersion.bind(this) }, 'Restore original version.')); 377 | } 378 | return h('div', { className: 'editor' }, localVersionDiv, 379 | h('div', { className: 'editor__source-wrap'}, 380 | h('div', { className: 'editor__toolbar' }, 381 | h('button', { class: 'button', onClick: this.onGenerate.bind(this) }, 'Generate'), 382 | h(SeedPicker, { seed: this.state.seed, onSetSeed: this.onSetSeed.bind(this), onPrevSeed: this.onPrevSeed.bind(this), onNextSeed: this.onNextSeed.bind(this) }) 383 | ), 384 | h('div', { className: 'editor__source' }, 385 | h(Source, { source, loading: this.state.loading, importError: props.importError, onSourceChanged: this.onSourceChanged.bind(this) }) 386 | ), 387 | debugView 388 | ), 389 | h('div', { className: 'editor__viewer-wrap'}, 390 | h('span', { class: 'editor__toolbar' }, 391 | h('button', { class: 'button', onClick: this.onTogglePlay.bind(this) }, state.playing ? 'Stop' : 'Play'), 392 | h('span', { className: 'editor__toolbar-right' }, 393 | props.headerRight 394 | ) 395 | ), 396 | h('div', { className: 'editor__viewer' }, 397 | h('div', { className: 'editor__result', dangerouslySetInnerHTML: { __html: this.state.result } }) 398 | ) 399 | ) 400 | ); 401 | } 402 | } 403 | 404 | Editor.prototype.onInput = debounce(Editor.prototype.onInput, 200); 405 | 406 | class Sketch extends Component { 407 | constructor(props) { 408 | super(props); 409 | this.state = { saving: false, unsaved: false, source: undefined, seed: undefined }; 410 | } 411 | 412 | onImportError() { 413 | this.setState({ importError: true }); 414 | } 415 | 416 | setRemoteSource(remoteSource) { 417 | this.setState({ remoteSource }); 418 | } 419 | 420 | onSourceChanged(source, initialLoad, localSource=false) { 421 | this.setState({ unsaved: localSource || !initialLoad, source }); 422 | if (!initialLoad) { 423 | const key = this.props.id || 'empty'; 424 | window.localStorage.setItem(key, source); 425 | } 426 | } 427 | 428 | onSeedChanged(seed, initialLoad=false) { 429 | this.setState({ seed }); 430 | if (!initialLoad && this.props.id !== undefined) { 431 | window.history.replaceState('', '', `${window.location.protocol}\/\/${window.location.host}/sketch/${this.props.id}?seed=${seed}`); 432 | } 433 | } 434 | 435 | componentDidMount() { 436 | window.addEventListener('beforeunload', (e) => { 437 | let confirm = null; 438 | if (this.state.unsaved) { 439 | confirm = 'You have unsaved changes.'; 440 | (e || window.event).returnValue = confirm; 441 | } 442 | return confirm; 443 | }); 444 | } 445 | 446 | async onSave() { 447 | console.assert(this.state.source !== undefined); 448 | console.assert(this.state.seed !== undefined); 449 | if (this.state.saving) return; 450 | this.setState({ saving: true }); 451 | let sketch = {}; 452 | sketch.source = this.state.source; 453 | sketch.seed = this.state.seed; 454 | if (this.props.id) sketch.parent = this.props.id; 455 | const res = await fetch(new Request(`${SKETCH_REST_URL}.json`), { 456 | method: 'POST', 457 | body: JSON.stringify(sketch), 458 | headers: new Headers({'Content-Type': 'application/json'}) 459 | }); 460 | const json = await res.json(); 461 | if (json) { 462 | const ref = json.name; 463 | this.setState({ saving: false, unsaved: false }); 464 | window.localStorage.removeItem(this.props.id || 'empty'); 465 | this.setRemoteSource(undefined); 466 | window.history.replaceState('', '', `${window.location.protocol}\/\/${window.location.host}/sketch/${json.name}`); 467 | route(`/sketch/${json.name}`); 468 | } else { 469 | throw new Error('Error: Could not save sketch'); 470 | } 471 | } 472 | 473 | render(props, state) { 474 | let saveLabel = state.saving ? 'Saving...' : 'Save'; 475 | return h('div', {class: 'app'}, 476 | h(Header, { unsaved: !!state.unsaved }, 477 | h('button', {class: 'button save-button' + (state.unsaved ? ' unsaved' : '') + (state.importError ? ' disabled': ''), onClick: this.onSave.bind(this), disabled: state.saving}, saveLabel) 478 | ), 479 | h(Editor, { 480 | id: props.id, 481 | remoteSource: this.state.remoteSource, 482 | setRemoteSource: this.setRemoteSource.bind(this), 483 | importError: state.importError, 484 | onImportError: this.onImportError.bind(this), 485 | onSourceChanged: this.onSourceChanged.bind(this), 486 | onSeedChanged: this.onSeedChanged.bind(this), 487 | headerRight: h('a', { href:'/docs', target: '_blank' }, 'Documentation') 488 | }) 489 | ); 490 | } 491 | } 492 | 493 | class Embed extends Component { 494 | render(props) { 495 | const link = h('a', { href:`/sketch/${ props.id }`, target: '_blank' }, 'Open in new window'); 496 | return h('div', {class: 'app embed'}, 497 | h(Editor, { id: props.id, headerRight: link }) 498 | ); 499 | } 500 | } 501 | 502 | class Docs extends Component { 503 | constructor(props) { 504 | super(props); 505 | this.state = { page: undefined, html: 'Loading...' }; 506 | } 507 | 508 | render(props) { 509 | const PAGES = [ 510 | { id: 'index', title: 'Documentation' }, 511 | { id: 'getting-started', title: 'Getting Started' }, 512 | { id: 'graphics', title: 'Generating Graphics' }, 513 | { id: 'animation', title: 'Animation' }, 514 | { id: 'recursion', title: 'Recursion' }, 515 | { id: 'css-tricks', title: 'Using CSS functionality' }, 516 | { id: 'arguments', title: 'Working with arguments' }, 517 | { id: 'cheat-sheet', title: 'Cheat Sheet' }, 518 | ] 519 | const items = []; 520 | for (const page of PAGES) { 521 | let link; 522 | if (page.id === 'index') { 523 | link = h(Link, { class: 'docs__header' + (props.page === 'index' ? ' active' : ''), href: '/docs' }, page.title); 524 | } else { 525 | link = h(Link, { class: props.page === page.id ? 'active' : '', href: `/docs/${ page.id }` }, page.title); 526 | } 527 | items.push(h('li', {}, link)); 528 | } 529 | return h('div', {class: 'app'}, 530 | h(Header, {}, 531 | h('a', {class: 'button', href: '/sketch'}, 'New Sketch') 532 | ), 533 | h('div', {class: 'page docs'}, 534 | h('div', {class: 'docs__nav'}, 535 | h('ul', {}, 536 | items 537 | ) 538 | ), 539 | h('div', {class: 'docs__body', dangerouslySetInnerHTML: { __html: this.state.html}}) 540 | ) 541 | ); 542 | } 543 | 544 | onPage() { 545 | if (this.state.page === this.props.page) return; 546 | const page = this.props.page || 'index'; 547 | fetch(`/_docs/${page}.md`) 548 | .then(res => res.text()) 549 | .then(text => { 550 | const html = marked(text, { renderer: gMarkedRenderer }); 551 | this.setState({ page: this.props.page, html }); 552 | }); 553 | } 554 | 555 | componentDidMount() { 556 | this.onPage(); 557 | } 558 | 559 | componentDidUpdate() { 560 | this.onPage(); 561 | if (window.location.pathname.split('/')[1] === 'docs') { 562 | let cw = document.getElementsByClassName('code-wrap'); 563 | for (let i = 0; i < cw.length; i += 1) { 564 | let el = cw[i]; 565 | let code = el.getElementsByTagName('code')[0]; 566 | let codeResult = el.getElementsByClassName('code-result')[0]; 567 | if (code !== undefined && codeResult !== undefined) { 568 | parsePhraseBook(code.textContent, loadSketch) 569 | .then(phraseBook => generateString(phraseBook)) 570 | .then(result => { codeResult.innerHTML = result; }) 571 | .catch(err => { codeResult.innerHTML = err; }); 572 | } 573 | } 574 | } 575 | } 576 | } 577 | 578 | class View extends Component { 579 | constructor(props) { 580 | super(props); 581 | this.state = { result: 'Loading...' }; 582 | this.load(); 583 | } 584 | 585 | async load() { 586 | const json = await loadSketch(this.props.id); 587 | if (json === null) { 588 | const err = `Error: Could not import sketch named "${this.props.id}".`; 589 | this.setState({ loading: false, debugOutput: err, source: err }); 590 | if (this.props.onImportError) { 591 | this.props.onImportError(); 592 | } 593 | return; 594 | } 595 | const sketch = Object.assign({ key: this.props.id }, json); 596 | let newState = { loading: false, source: sketch.source }; 597 | const urlSeed = getURLParameter('seed'); 598 | if (urlSeed) { 599 | newState.seed = urlSeed; 600 | } else if (sketch.seed) { 601 | newState.seed = sketch.seed; 602 | } else { 603 | newState.seed = this.state.seed; 604 | } 605 | this.setState(newState); 606 | this.generate(); 607 | } 608 | 609 | async generate(parse=true) { 610 | try { 611 | if (parse) { 612 | this.phraseBook = await parsePhraseBook(this.state.source, loadSketch); 613 | } 614 | const result = await generateString(this.phraseBook, 'root', {}, this.state.seed); 615 | console.log(result); 616 | this.setState({ result: result, debugOutput: '' }); 617 | } catch (e) { 618 | this.setState({ debugOutput: e.message }); 619 | } 620 | } 621 | 622 | onSetSeed(seed) { 623 | this.setState({seed}); 624 | if (this.props.onSeedChanged) { 625 | this.props.onSeedChanged(seed); 626 | } 627 | this.generate(false); 628 | } 629 | 630 | onGenerate() { 631 | let seed = nextTextSeed(this.state.seed); 632 | console.log(seed); 633 | this.setState({ seed }); 634 | this.generate(false); 635 | } 636 | 637 | render(props) { 638 | return h('div', {class: 'app view', onClick: this.onGenerate.bind(this) }, 639 | h('div', {class: 'docs__body', dangerouslySetInnerHTML: { __html: this.state.result}}) 640 | ); 641 | } 642 | } 643 | 644 | class NotFound extends Component { 645 | render() { 646 | return h('div', {class: 'app'}, 647 | h(Header, {}), 648 | h('div', {class: 'page centered'}, 649 | h('h1', {}, 'Page not found.'), 650 | h('a', {href: '/'}, 'Go Back Home') 651 | ) 652 | ); 653 | } 654 | } 655 | 656 | class App extends Component { 657 | render() { 658 | return h(Router, {}, 659 | h(Home, { path: '/' }), 660 | h(Sketch, { path: '/sketch' }), 661 | h(Sketch, { path: '/sketch/:id' }), 662 | h(Embed, { path: '/embed' }), 663 | h(Embed, { path: '/embed/:id' }), 664 | h(View, { path: '/view/:id' }), 665 | h(Docs, { path: '/docs', page: 'index'}), 666 | h(Docs, { path: '/docs/:page'}), 667 | h(NotFound, { type: '404', default: true }) 668 | ); 669 | } 670 | } 671 | 672 | render(h(App), document.body); 673 | -------------------------------------------------------------------------------- /third_party/bundle.min.js: -------------------------------------------------------------------------------- 1 | /* marked.min.js */ 2 | (function(){function u(a){this.tokens=[];this.tokens.links={};this.options=a||m.defaults;this.rules=f.normal;this.options.gfm&&(this.rules=this.options.tables?f.tables:f.gfm)}function r(a,b){this.options=b||m.defaults;this.links=a;this.rules=k.normal;this.renderer=this.options.renderer||new l;this.renderer.options=this.options;if(!this.links)throw Error("Tokens array requires a `links` property.");this.options.gfm?this.rules=this.options.breaks?k.breaks:k.gfm:this.options.pedantic&&(this.rules=k.pedantic)} function l(a){this.options=a||{}}function q(a){this.tokens=[];this.token=null;this.options=a||m.defaults;this.options.renderer=this.options.renderer||new l;this.renderer=this.options.renderer;this.renderer.options=this.options}function n(a,b){return a.replace(b?/&/g:/&(?!#?\w+;)/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function w(a){return a.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g,function(a,e){e=e.toLowerCase();return"colon"=== e?":":"#"===e.charAt(0)?"x"===e.charAt(1)?String.fromCharCode(parseInt(e.substring(2),16)):String.fromCharCode(+e.substring(1)):""})}function p(a,b){a=a.source;b=b||"";return function d(c,f){if(!c)return new RegExp(a,b);f=f.source||f;f=f.replace(/(^|[^\[])\^/g,"$1");a=a.replace(c,f);return d}}function v(){}function t(a){for(var b=1,e,c;bc.length)return h();delete b.highlight;if(!k)return h();for(;fAn error occured:

"+n(g.message+"",!0)+"
";throw g;}}var f={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:v,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:v,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:v,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/,bullet:/(?:[*+-]|\d+\.)/,item:/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/};f.item=p(f.item,"gm")(/bull/g,f.bullet)();f.list=p(f.list)(/bull/g,f.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+f.def.source+")")();f.blockquote= p(f.blockquote)("def",f.def)();f._tag="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b";f.html=p(f.html)("comment",/\x3c!--[\s\S]*?--\x3e/)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,f._tag)();f.paragraph=p(f.paragraph)("hr",f.hr)("heading",f.heading)("lheading",f.lheading)("blockquote",f.blockquote)("tag","<"+f._tag)("def",f.def)();f.normal= t({},f);f.gfm=t({},f.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/});f.gfm.paragraph=p(f.paragraph)("(?!","(?!"+f.gfm.fences.source.replace("\\1","\\2")+"|"+f.list.source.replace("\\1","\\3")+"|")();f.tables=t({},f.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/});u.rules=f;u.lex=function(a,b){return(new u(b)).lex(a)}; u.prototype.lex=function(a){a=a.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n");return this.token(a,!0)};u.prototype.token=function(a,b,e){a=a.replace(/^ +$/gm,"");for(var c,k,d,m,h,g,l;a;){if(d=this.rules.newline.exec(a))a=a.substring(d[0].length),1 ?/gm,""),this.token(d,b,!0),this.tokens.push({type:"blockquote_end"});else if(d=this.rules.list.exec(a)){a=a.substring(d[0].length);m=d[2];this.tokens.push({type:"list_start",ordered:1])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:v,tag:/^\x3c!--[\s\S]*?--\x3e|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, br:/^ {2,}\n(?!\s*$)/,del:v,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/};k.link=p(k.link)("inside",k._inside)("href",k._href)();k.reflink=p(k.reflink)("inside",k._inside)();k.normal=t({},k);k.pedantic=t({},k.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/});k.gfm=t({},k.normal,{escape:p(k.escape)("])","~|])")(), url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:p(k.text)("]|","~]|")("|","|https?://|")()});k.breaks=t({},k.gfm,{br:p(k.br)("{2,}","*")(),text:p(k.gfm.text)("{2,}","*")()});r.rules=k;r.output=function(a,b,e){return(new r(b,e)).output(a)};r.prototype.output=function(a){for(var b="",e,c;a;)if(c=this.rules.escape.exec(a))a=a.substring(c[0].length),b+=c[1];else if(c=this.rules.autolink.exec(a))a=a.substring(c[0].length),"@"===c[2]?(e=":"===c[1].charAt(6)?this.mangle(c[1].substring(7)): this.mangle(c[1]),c=this.mangle("mailto:")+e):c=e=n(c[1]),b+=this.renderer.link(c,null,e);else if(!this.inLink&&(c=this.rules.url.exec(a)))a=a.substring(c[0].length),c=e=n(c[1]),b+=this.renderer.link(c,null,e);else if(c=this.rules.tag.exec(a))!this.inLink&&/^/i.test(c[0])&&(this.inLink=!1),a=a.substring(c[0].length),b+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(c[0]):n(c[0]):c[0];else if(c=this.rules.link.exec(a))a=a.substring(c[0].length), this.inLink=!0,b+=this.outputLink(c,{href:c[2],title:c[3]}),this.inLink=!1;else if((c=this.rules.reflink.exec(a))||(c=this.rules.nolink.exec(a)))a=a.substring(c[0].length),e=(c[2]||c[1]).replace(/\s+/g," "),(e=this.links[e.toLowerCase()])&&e.href?(this.inLink=!0,b+=this.outputLink(c,e),this.inLink=!1):(b+=c[0].charAt(0),a=c[0].substring(1)+a);else if(c=this.rules.strong.exec(a))a=a.substring(c[0].length),b+=this.renderer.strong(this.output(c[2]||c[1]));else if(c=this.rules.em.exec(a))a=a.substring(c[0].length), b+=this.renderer.em(this.output(c[2]||c[1]));else if(c=this.rules.code.exec(a))a=a.substring(c[0].length),b+=this.renderer.codespan(n(c[2],!0));else if(c=this.rules.br.exec(a))a=a.substring(c[0].length),b+=this.renderer.br();else if(c=this.rules.del.exec(a))a=a.substring(c[0].length),b+=this.renderer.del(this.output(c[1]));else if(c=this.rules.text.exec(a))a=a.substring(c[0].length),b+=this.renderer.text(n(this.smartypants(c[0])));else if(a)throw Error("Infinite loop on byte: "+a.charCodeAt(0));return b}; r.prototype.outputLink=function(a,b){var e=n(b.href),c=b.title?n(b.title):null;return"!"!==a[0].charAt(0)?this.renderer.link(e,c,this.output(a[1])):this.renderer.image(e,c,n(a[1]))};r.prototype.smartypants=function(a){return this.options.smartypants?a.replace(/---/g,"\u2014").replace(/--/g,"\u2013").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1\u2018").replace(/'/g,"\u2019").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1\u201c").replace(/"/g,"\u201d").replace(/\.{3}/g,"\u2026"):a};r.prototype.mangle=function(a){if(!this.options.mangle)return a; for(var b="",e=a.length,c=0,f;c'+(e?a:n(a,!0))+"\n\n":"
"+(e?a:n(a,!0))+"\n
"};l.prototype.blockquote=function(a){return"
\n"+a+"
\n"};l.prototype.html=function(a){return a}; l.prototype.heading=function(a,b,e){return"'+a+"\n"};l.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"};l.prototype.list=function(a,b){var e=b?"ol":"ul";return"<"+e+">\n"+a+"\n"};l.prototype.listitem=function(a){return"
  • "+a+"
  • \n"};l.prototype.paragraph=function(a){return"

    "+a+"

    \n"};l.prototype.table=function(a,b){return"\n\n"+a+"\n\n"+b+ "\n
    \n"};l.prototype.tablerow=function(a){return"\n"+a+"\n"};l.prototype.tablecell=function(a,b){var e=b.header?"th":"td";return(b.align?"<"+e+' style="text-align:'+b.align+'">':"<"+e+">")+a+"\n"};l.prototype.strong=function(a){return""+a+""};l.prototype.em=function(a){return""+a+""};l.prototype.codespan=function(a){return""+a+""};l.prototype.br=function(){return this.options.xhtml?"
    ":"
    "};l.prototype.del=function(a){return""+ a+""};l.prototype.link=function(a,b,e){if(this.options.sanitize){try{var c=decodeURIComponent(w(a)).replace(/[^\w:]/g,"").toLowerCase()}catch(y){return""}if(0===c.indexOf("javascript:")||0===c.indexOf("vbscript:")||0===c.indexOf("data:"))return""}a='
    "+e+"")};l.prototype.image=function(a,b,e){a=''+e+'":">"};l.prototype.text=function(a){return a};q.parse= function(a,b,e){return(new q(b,e)).parse(a)};q.prototype.parse=function(a){this.inline=new r(a.links,this.options,this.renderer);this.tokens=a.reverse();for(a="";this.next();)a+=this.tok();return a};q.prototype.next=function(){return this.token=this.tokens.pop()};q.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0};q.prototype.parseText=function(){for(var a=this.token.text;"text"===this.peek().type;)a+="\n"+this.next().text;return this.inline.output(a)};q.prototype.tok=function(){switch(this.token.type){case "space":return""; case "hr":return this.renderer.hr();case "heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text);case "code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case "table":var a="",b="",e,c;var f="";for(e=0;e2;)W.push(arguments[l]);n&&null!=n.children&&(W.length||W.push(n.children),delete n.children);while(W.length)if((r=W.pop())&&void 0!==r.pop)for(l=r.length;l--;)W.push(r[l]);else"boolean"==typeof r&&(r=null),(i="function"!=typeof t)&&(null==r?r="":"number"==typeof r?r+="":"string"!=typeof r&&(i=!1)),i&&o?a[a.length-1]+=r:a===E?a=[r]:a.push(r),o=i;var u=new e;return u.nodeName=t,u.children=a,u.attributes=null==n?void 0:n,u.key=null==n?void 0:n.key,void 0!==S.vnode&&S.vnode(u),u}function n(e,t){for(var n in t)e[n]=t[n];return e}function o(e,o){return t(e.nodeName,n(n({},e.attributes),o),arguments.length>2?[].slice.call(arguments,2):e.children)}function r(e){!e.__d&&(e.__d=!0)&&1==A.push(e)&&(S.debounceRendering||P)(i)}function i(){var e,t=A;A=[];while(e=t.pop())e.__d&&k(e)}function l(e,t,n){return"string"==typeof t||"number"==typeof t?void 0!==e.splitText:"string"==typeof t.nodeName?!e._componentConstructor&&a(e,t.nodeName):n||e._componentConstructor===t.nodeName}function a(e,t){return e.__n===t||e.nodeName.toLowerCase()===t.toLowerCase()}function u(e){var t=n({},e.attributes);t.children=e.children;var o=e.nodeName.defaultProps;if(void 0!==o)for(var r in o)void 0===t[r]&&(t[r]=o[r]);return t}function _(e,t){var n=t?document.createElementNS("http://www.w3.org/2000/svg",e):document.createElement(e);return n.__n=e,n}function p(e){var t=e.parentNode;t&&t.removeChild(e)}function c(e,t,n,o,r){if("className"===t&&(t="class"),"key"===t);else if("ref"===t)n&&n(null),o&&o(e);else if("class"!==t||r)if("style"===t){if(o&&"string"!=typeof o&&"string"!=typeof n||(e.style.cssText=o||""),o&&"object"==typeof o){if("string"!=typeof n)for(var i in n)i in o||(e.style[i]="");for(var i in o)e.style[i]="number"==typeof o[i]&&!1===V.test(i)?o[i]+"px":o[i]}}else if("dangerouslySetInnerHTML"===t)o&&(e.innerHTML=o.__html||"");else if("o"==t[0]&&"n"==t[1]){var l=t!==(t=t.replace(/Capture$/,""));t=t.toLowerCase().substring(2),o?n||e.addEventListener(t,f,l):e.removeEventListener(t,f,l),(e.__l||(e.__l={}))[t]=o}else if("list"!==t&&"type"!==t&&!r&&t in e)s(e,t,null==o?"":o),null!=o&&!1!==o||e.removeAttribute(t);else{var a=r&&t!==(t=t.replace(/^xlink\:?/,""));null==o||!1===o?a?e.removeAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase()):e.removeAttribute(t):"function"!=typeof o&&(a?e.setAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase(),o):e.setAttribute(t,o))}else e.className=o||""}function s(e,t,n){try{e[t]=n}catch(e){}}function f(e){return this.__l[e.type](S.event&&S.event(e)||e)}function d(){var e;while(e=D.pop())S.afterMount&&S.afterMount(e),e.componentDidMount&&e.componentDidMount()}function h(e,t,n,o,r,i){H++||(R=null!=r&&void 0!==r.ownerSVGElement,j=null!=e&&!("__preactattr_"in e));var l=m(e,t,n,o,i);return r&&l.parentNode!==r&&r.appendChild(l),--H||(j=!1,i||d()),l}function m(e,t,n,o,r){var i=e,l=R;if(null!=t&&"boolean"!=typeof t||(t=""),"string"==typeof t||"number"==typeof t)return e&&void 0!==e.splitText&&e.parentNode&&(!e._component||r)?e.nodeValue!=t&&(e.nodeValue=t):(i=document.createTextNode(t),e&&(e.parentNode&&e.parentNode.replaceChild(i,e),b(e,!0))),i.__preactattr_=!0,i;var u=t.nodeName;if("function"==typeof u)return U(e,t,n,o);if(R="svg"===u||"foreignObject"!==u&&R,u+="",(!e||!a(e,u))&&(i=_(u,R),e)){while(e.firstChild)i.appendChild(e.firstChild);e.parentNode&&e.parentNode.replaceChild(i,e),b(e,!0)}var p=i.firstChild,c=i.__preactattr_,s=t.children;if(null==c){c=i.__preactattr_={};for(var f=i.attributes,d=f.length;d--;)c[f[d].name]=f[d].value}return!j&&s&&1===s.length&&"string"==typeof s[0]&&null!=p&&void 0!==p.splitText&&null==p.nextSibling?p.nodeValue!=s[0]&&(p.nodeValue=s[0]):(s&&s.length||null!=p)&&v(i,s,n,o,j||null!=c.dangerouslySetInnerHTML),g(i,t.attributes,c),R=l,i}function v(e,t,n,o,r){var i,a,u,_,c,s=e.childNodes,f=[],d={},h=0,v=0,y=s.length,g=0,w=t?t.length:0;if(0!==y)for(var C=0;Ce.rank?-1:t.index-e.index}function o(t,e){return t.index=e,t.rank=p(t),t.attributes}function i(t){return t.replace(/(^\/+|\/+$)/g,"").split("/")}function u(t){return":"==t.charAt(0)?1+"*+?".indexOf(t.charAt(t.length-1))||4:5}function a(t){return i(t).map(u).join("")}function p(t){return t.attributes.default?0:a(t.attributes.path)}function c(t){return null!=t.__preactattr_||"undefined"!=typeof Symbol&&null!=t[Symbol.for("preactattr")]}function f(t,e){void 0===e&&(e="push"),R&&R[e]?R[e](t):"undefined"!=typeof history&&history[e+"State"]&&history[e+"State"](null,null,t)}function l(){var t;return t=R&&R.location?R.location:R&&R.getCurrentLocation?R.getCurrentLocation():"undefined"!=typeof location?location:x,""+(t.pathname||"")+(t.search||"")}function s(t,e){return void 0===e&&(e=!1),"string"!=typeof t&&t.url&&(e=t.replace,t=t.url),h(t)&&f(t,e?"replace":"push"),d(t)}function h(t){for(var e=U.length;e--;)if(U[e].canRoute(t))return!0;return!1}function d(t){for(var e=!1,n=0;n0},u.prototype.routeTo=function(t){return this._didRoute=!1,this.setState({url:t}),this.updating?this.canRoute(t):(this.forceUpdate(),this._didRoute)},u.prototype.componentWillMount=function(){U.push(this),this.updating=!0},u.prototype.componentDidMount=function(){var t=this;R&&(this.unlisten=R.listen(function(e){t.routeTo(""+(e.pathname||"")+(e.search||""))})),this.updating=!1},u.prototype.componentWillUnmount=function(){"function"==typeof this.unlisten&&this.unlisten(),U.splice(U.indexOf(this),1)},u.prototype.componentWillUpdate=function(){this.updating=!0},u.prototype.componentDidUpdate=function(){this.updating=!1},u.prototype.getMatchingChildren=function(i,u,a){return i.filter(o).sort(r).map(function(r){var o=n(u,r.attributes.path,r.attributes);if(o){if(a!==!1){var i={url:u,matches:o};return e(i,o),delete i.ref,delete i.key,t.cloneElement(r,i)}return r}}).filter(Boolean)},u.prototype.render=function(t,e){var n=t.children,r=t.onChange,o=e.url,i=this.getMatchingChildren(n,o,!0),u=i[0]||null;this._didRoute=!!u;var a=this.previousUrl;return o!==a&&(this.previousUrl=o,"function"==typeof r&&r({router:this,url:o,previous:a,active:i,current:u})),u},u}(t.Component),I=function(n){return t.h("a",e({onClick:m},n))},L=function(e){return t.h(e.component,e)};return A.subscribers=k,A.getCurrentUrl=l,A.route=s,A.Router=A,A.Route=L,A.Link=I,A}); 9 | 10 | /* seedrandom.min.js */ 11 | !function(a,b){function c(c,j,k){var n=[];j=1==j?{entropy:!0}:j||{};var s=g(f(j.entropy?[c,i(a)]:null==c?h():c,3),n),t=new d(n),u=function(){for(var a=t.g(m),b=p,c=0;a=r;)a/=2,b/=2,c>>>=1;return(a+c)/b};return u.int32=function(){return 0|t.g(4)},u.quick=function(){return t.g(4)/4294967296},u.double=u,g(i(t.S),a),(j.pass||k||function(a,c,d,f){return f&&(f.S&&e(f,t),a.state=function(){return e(t,{})}),d?(b[o]=a,c):a})(u,s,"global"in j?j.global:this==b,j.state)}function d(a){var b,c=a.length,d=this,e=0,f=d.i=d.j=0,g=d.S=[];for(c||(a=[c++]);e/g,">").replace(/"/g,""").replace(/'/g,"'")}function w(a){return a.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g,function(a,e){e=e.toLowerCase();return"colon"=== 3 | e?":":"#"===e.charAt(0)?"x"===e.charAt(1)?String.fromCharCode(parseInt(e.substring(2),16)):String.fromCharCode(+e.substring(1)):""})}function p(a,b){a=a.source;b=b||"";return function d(c,f){if(!c)return new RegExp(a,b);f=f.source||f;f=f.replace(/(^|[^\[])\^/g,"$1");a=a.replace(c,f);return d}}function v(){}function t(a){for(var b=1,e,c;bc.length)return h();delete b.highlight;if(!k)return h();for(;fAn error occured:

    "+n(g.message+"",!0)+"
    ";throw g;}}var f={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:v,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:v,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, 6 | html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:v,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/,bullet:/(?:[*+-]|\d+\.)/,item:/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/};f.item=p(f.item,"gm")(/bull/g,f.bullet)();f.list=p(f.list)(/bull/g,f.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+f.def.source+")")();f.blockquote= 7 | p(f.blockquote)("def",f.def)();f._tag="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b";f.html=p(f.html)("comment",/\x3c!--[\s\S]*?--\x3e/)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,f._tag)();f.paragraph=p(f.paragraph)("hr",f.hr)("heading",f.heading)("lheading",f.lheading)("blockquote",f.blockquote)("tag","<"+f._tag)("def",f.def)();f.normal= 8 | t({},f);f.gfm=t({},f.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/});f.gfm.paragraph=p(f.paragraph)("(?!","(?!"+f.gfm.fences.source.replace("\\1","\\2")+"|"+f.list.source.replace("\\1","\\3")+"|")();f.tables=t({},f.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/});u.rules=f;u.lex=function(a,b){return(new u(b)).lex(a)}; 9 | u.prototype.lex=function(a){a=a.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n");return this.token(a,!0)};u.prototype.token=function(a,b,e){a=a.replace(/^ +$/gm,"");for(var c,k,d,m,h,g,l;a;){if(d=this.rules.newline.exec(a))a=a.substring(d[0].length),1 ?/gm,""),this.token(d,b,!0),this.tokens.push({type:"blockquote_end"});else if(d=this.rules.list.exec(a)){a=a.substring(d[0].length);m=d[2];this.tokens.push({type:"list_start",ordered:1])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:v,tag:/^\x3c!--[\s\S]*?--\x3e|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, 17 | br:/^ {2,}\n(?!\s*$)/,del:v,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/};k.link=p(k.link)("inside",k._inside)("href",k._href)();k.reflink=p(k.reflink)("inside",k._inside)();k.normal=t({},k);k.pedantic=t({},k.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/});k.gfm=t({},k.normal,{escape:p(k.escape)("])","~|])")(), 18 | url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:p(k.text)("]|","~]|")("|","|https?://|")()});k.breaks=t({},k.gfm,{br:p(k.br)("{2,}","*")(),text:p(k.gfm.text)("{2,}","*")()});r.rules=k;r.output=function(a,b,e){return(new r(b,e)).output(a)};r.prototype.output=function(a){for(var b="",e,c;a;)if(c=this.rules.escape.exec(a))a=a.substring(c[0].length),b+=c[1];else if(c=this.rules.autolink.exec(a))a=a.substring(c[0].length),"@"===c[2]?(e=":"===c[1].charAt(6)?this.mangle(c[1].substring(7)): 19 | this.mangle(c[1]),c=this.mangle("mailto:")+e):c=e=n(c[1]),b+=this.renderer.link(c,null,e);else if(!this.inLink&&(c=this.rules.url.exec(a)))a=a.substring(c[0].length),c=e=n(c[1]),b+=this.renderer.link(c,null,e);else if(c=this.rules.tag.exec(a))!this.inLink&&/^/i.test(c[0])&&(this.inLink=!1),a=a.substring(c[0].length),b+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(c[0]):n(c[0]):c[0];else if(c=this.rules.link.exec(a))a=a.substring(c[0].length), 20 | this.inLink=!0,b+=this.outputLink(c,{href:c[2],title:c[3]}),this.inLink=!1;else if((c=this.rules.reflink.exec(a))||(c=this.rules.nolink.exec(a)))a=a.substring(c[0].length),e=(c[2]||c[1]).replace(/\s+/g," "),(e=this.links[e.toLowerCase()])&&e.href?(this.inLink=!0,b+=this.outputLink(c,e),this.inLink=!1):(b+=c[0].charAt(0),a=c[0].substring(1)+a);else if(c=this.rules.strong.exec(a))a=a.substring(c[0].length),b+=this.renderer.strong(this.output(c[2]||c[1]));else if(c=this.rules.em.exec(a))a=a.substring(c[0].length), 21 | b+=this.renderer.em(this.output(c[2]||c[1]));else if(c=this.rules.code.exec(a))a=a.substring(c[0].length),b+=this.renderer.codespan(n(c[2],!0));else if(c=this.rules.br.exec(a))a=a.substring(c[0].length),b+=this.renderer.br();else if(c=this.rules.del.exec(a))a=a.substring(c[0].length),b+=this.renderer.del(this.output(c[1]));else if(c=this.rules.text.exec(a))a=a.substring(c[0].length),b+=this.renderer.text(n(this.smartypants(c[0])));else if(a)throw Error("Infinite loop on byte: "+a.charCodeAt(0));return b}; 22 | r.prototype.outputLink=function(a,b){var e=n(b.href),c=b.title?n(b.title):null;return"!"!==a[0].charAt(0)?this.renderer.link(e,c,this.output(a[1])):this.renderer.image(e,c,n(a[1]))};r.prototype.smartypants=function(a){return this.options.smartypants?a.replace(/---/g,"\u2014").replace(/--/g,"\u2013").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1\u2018").replace(/'/g,"\u2019").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1\u201c").replace(/"/g,"\u201d").replace(/\.{3}/g,"\u2026"):a};r.prototype.mangle=function(a){if(!this.options.mangle)return a; 23 | for(var b="",e=a.length,c=0,f;c'+(e?a:n(a,!0))+"\n\n":"
    "+(e?a:n(a,!0))+"\n
    "};l.prototype.blockquote=function(a){return"
    \n"+a+"
    \n"};l.prototype.html=function(a){return a}; 24 | l.prototype.heading=function(a,b,e){return"'+a+"\n"};l.prototype.hr=function(){return this.options.xhtml?"
    \n":"
    \n"};l.prototype.list=function(a,b){var e=b?"ol":"ul";return"<"+e+">\n"+a+"\n"};l.prototype.listitem=function(a){return"
  • "+a+"
  • \n"};l.prototype.paragraph=function(a){return"

    "+a+"

    \n"};l.prototype.table=function(a,b){return"\n\n"+a+"\n\n"+b+ 25 | "\n
    \n"};l.prototype.tablerow=function(a){return"\n"+a+"\n"};l.prototype.tablecell=function(a,b){var e=b.header?"th":"td";return(b.align?"<"+e+' style="text-align:'+b.align+'">':"<"+e+">")+a+"\n"};l.prototype.strong=function(a){return""+a+""};l.prototype.em=function(a){return""+a+""};l.prototype.codespan=function(a){return""+a+""};l.prototype.br=function(){return this.options.xhtml?"
    ":"
    "};l.prototype.del=function(a){return""+ 26 | a+""};l.prototype.link=function(a,b,e){if(this.options.sanitize){try{var c=decodeURIComponent(w(a)).replace(/[^\w:]/g,"").toLowerCase()}catch(y){return""}if(0===c.indexOf("javascript:")||0===c.indexOf("vbscript:")||0===c.indexOf("data:"))return""}a='
    "+e+"")};l.prototype.image=function(a,b,e){a=''+e+'":">"};l.prototype.text=function(a){return a};q.parse= 27 | function(a,b,e){return(new q(b,e)).parse(a)};q.prototype.parse=function(a){this.inline=new r(a.links,this.options,this.renderer);this.tokens=a.reverse();for(a="";this.next();)a+=this.tok();return a};q.prototype.next=function(){return this.token=this.tokens.pop()};q.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0};q.prototype.parseText=function(){for(var a=this.token.text;"text"===this.peek().type;)a+="\n"+this.next().text;return this.inline.output(a)};q.prototype.tok=function(){switch(this.token.type){case "space":return""; 28 | case "hr":return this.renderer.hr();case "heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text);case "code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case "table":var a="",b="",e,c;var f="";for(e=0;ee.rank?-1:t.index-e.index}function o(t,e){return t.index=e,t.rank=p(t),t.attributes}function i(t){return t.replace(/(^\/+|\/+$)/g,"").split("/")}function u(t){return":"==t.charAt(0)?1+"*+?".indexOf(t.charAt(t.length-1))||4:5}function a(t){return i(t).map(u).join("")}function p(t){return t.attributes.default?0:a(t.attributes.path)}function c(t){return null!=t.__preactattr_||"undefined"!=typeof Symbol&&null!=t[Symbol.for("preactattr")]}function f(t,e){void 0===e&&(e="push"),R&&R[e]?R[e](t):"undefined"!=typeof history&&history[e+"State"]&&history[e+"State"](null,null,t)}function l(){var t;return t=R&&R.location?R.location:R&&R.getCurrentLocation?R.getCurrentLocation():"undefined"!=typeof location?location:x,""+(t.pathname||"")+(t.search||"")}function s(t,e){return void 0===e&&(e=!1),"string"!=typeof t&&t.url&&(e=t.replace,t=t.url),h(t)&&f(t,e?"replace":"push"),d(t)}function h(t){for(var e=U.length;e--;)if(U[e].canRoute(t))return!0;return!1}function d(t){for(var e=!1,n=0;n0},u.prototype.routeTo=function(t){return this._didRoute=!1,this.setState({url:t}),this.updating?this.canRoute(t):(this.forceUpdate(),this._didRoute)},u.prototype.componentWillMount=function(){U.push(this),this.updating=!0},u.prototype.componentDidMount=function(){var t=this;R&&(this.unlisten=R.listen(function(e){t.routeTo(""+(e.pathname||"")+(e.search||""))})),this.updating=!1},u.prototype.componentWillUnmount=function(){"function"==typeof this.unlisten&&this.unlisten(),U.splice(U.indexOf(this),1)},u.prototype.componentWillUpdate=function(){this.updating=!0},u.prototype.componentDidUpdate=function(){this.updating=!1},u.prototype.getMatchingChildren=function(i,u,a){return i.filter(o).sort(r).map(function(r){var o=n(u,r.attributes.path,r.attributes);if(o){if(a!==!1){var i={url:u,matches:o};return e(i,o),delete i.ref,delete i.key,t.cloneElement(r,i)}return r}}).filter(Boolean)},u.prototype.render=function(t,e){var n=t.children,r=t.onChange,o=e.url,i=this.getMatchingChildren(n,o,!0),u=i[0]||null;this._didRoute=!!u;var a=this.previousUrl;return o!==a&&(this.previousUrl=o,"function"==typeof r&&r({router:this,url:o,previous:a,active:i,current:u})),u},u}(t.Component),I=function(n){return t.h("a",e({onClick:m},n))},L=function(e){return t.h(e.component,e)};return A.subscribers=k,A.getCurrentUrl=l,A.route=s,A.Router=A,A.Route=L,A.Link=I,A}); 2 | -------------------------------------------------------------------------------- /third_party/preact.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function e(){}function t(t,n){var o,r,i,l,a=E;for(l=arguments.length;l-- >2;)W.push(arguments[l]);n&&null!=n.children&&(W.length||W.push(n.children),delete n.children);while(W.length)if((r=W.pop())&&void 0!==r.pop)for(l=r.length;l--;)W.push(r[l]);else"boolean"==typeof r&&(r=null),(i="function"!=typeof t)&&(null==r?r="":"number"==typeof r?r+="":"string"!=typeof r&&(i=!1)),i&&o?a[a.length-1]+=r:a===E?a=[r]:a.push(r),o=i;var u=new e;return u.nodeName=t,u.children=a,u.attributes=null==n?void 0:n,u.key=null==n?void 0:n.key,void 0!==S.vnode&&S.vnode(u),u}function n(e,t){for(var n in t)e[n]=t[n];return e}function o(e,o){return t(e.nodeName,n(n({},e.attributes),o),arguments.length>2?[].slice.call(arguments,2):e.children)}function r(e){!e.__d&&(e.__d=!0)&&1==A.push(e)&&(S.debounceRendering||P)(i)}function i(){var e,t=A;A=[];while(e=t.pop())e.__d&&k(e)}function l(e,t,n){return"string"==typeof t||"number"==typeof t?void 0!==e.splitText:"string"==typeof t.nodeName?!e._componentConstructor&&a(e,t.nodeName):n||e._componentConstructor===t.nodeName}function a(e,t){return e.__n===t||e.nodeName.toLowerCase()===t.toLowerCase()}function u(e){var t=n({},e.attributes);t.children=e.children;var o=e.nodeName.defaultProps;if(void 0!==o)for(var r in o)void 0===t[r]&&(t[r]=o[r]);return t}function _(e,t){var n=t?document.createElementNS("http://www.w3.org/2000/svg",e):document.createElement(e);return n.__n=e,n}function p(e){var t=e.parentNode;t&&t.removeChild(e)}function c(e,t,n,o,r){if("className"===t&&(t="class"),"key"===t);else if("ref"===t)n&&n(null),o&&o(e);else if("class"!==t||r)if("style"===t){if(o&&"string"!=typeof o&&"string"!=typeof n||(e.style.cssText=o||""),o&&"object"==typeof o){if("string"!=typeof n)for(var i in n)i in o||(e.style[i]="");for(var i in o)e.style[i]="number"==typeof o[i]&&!1===V.test(i)?o[i]+"px":o[i]}}else if("dangerouslySetInnerHTML"===t)o&&(e.innerHTML=o.__html||"");else if("o"==t[0]&&"n"==t[1]){var l=t!==(t=t.replace(/Capture$/,""));t=t.toLowerCase().substring(2),o?n||e.addEventListener(t,f,l):e.removeEventListener(t,f,l),(e.__l||(e.__l={}))[t]=o}else if("list"!==t&&"type"!==t&&!r&&t in e)s(e,t,null==o?"":o),null!=o&&!1!==o||e.removeAttribute(t);else{var a=r&&t!==(t=t.replace(/^xlink\:?/,""));null==o||!1===o?a?e.removeAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase()):e.removeAttribute(t):"function"!=typeof o&&(a?e.setAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase(),o):e.setAttribute(t,o))}else e.className=o||""}function s(e,t,n){try{e[t]=n}catch(e){}}function f(e){return this.__l[e.type](S.event&&S.event(e)||e)}function d(){var e;while(e=D.pop())S.afterMount&&S.afterMount(e),e.componentDidMount&&e.componentDidMount()}function h(e,t,n,o,r,i){H++||(R=null!=r&&void 0!==r.ownerSVGElement,j=null!=e&&!("__preactattr_"in e));var l=m(e,t,n,o,i);return r&&l.parentNode!==r&&r.appendChild(l),--H||(j=!1,i||d()),l}function m(e,t,n,o,r){var i=e,l=R;if(null!=t&&"boolean"!=typeof t||(t=""),"string"==typeof t||"number"==typeof t)return e&&void 0!==e.splitText&&e.parentNode&&(!e._component||r)?e.nodeValue!=t&&(e.nodeValue=t):(i=document.createTextNode(t),e&&(e.parentNode&&e.parentNode.replaceChild(i,e),b(e,!0))),i.__preactattr_=!0,i;var u=t.nodeName;if("function"==typeof u)return U(e,t,n,o);if(R="svg"===u||"foreignObject"!==u&&R,u+="",(!e||!a(e,u))&&(i=_(u,R),e)){while(e.firstChild)i.appendChild(e.firstChild);e.parentNode&&e.parentNode.replaceChild(i,e),b(e,!0)}var p=i.firstChild,c=i.__preactattr_,s=t.children;if(null==c){c=i.__preactattr_={};for(var f=i.attributes,d=f.length;d--;)c[f[d].name]=f[d].value}return!j&&s&&1===s.length&&"string"==typeof s[0]&&null!=p&&void 0!==p.splitText&&null==p.nextSibling?p.nodeValue!=s[0]&&(p.nodeValue=s[0]):(s&&s.length||null!=p)&&v(i,s,n,o,j||null!=c.dangerouslySetInnerHTML),g(i,t.attributes,c),R=l,i}function v(e,t,n,o,r){var i,a,u,_,c,s=e.childNodes,f=[],d={},h=0,v=0,y=s.length,g=0,w=t?t.length:0;if(0!==y)for(var C=0;C=r;)a/=2,b/=2,c>>>=1;return(a+c)/b};return u.int32=function(){return 0|t.g(4)},u.quick=function(){return t.g(4)/4294967296},u.double=u,g(i(t.S),a),(j.pass||k||function(a,c,d,f){return f&&(f.S&&e(f,t),a.state=function(){return e(t,{})}),d?(b[o]=a,c):a})(u,s,"global"in j?j.global:this==b,j.state)}function d(a){var b,c=a.length,d=this,e=0,f=d.i=d.j=0,g=d.S=[];for(c||(a=[c++]);e