├── .gitignore ├── _headers ├── _redirects ├── netlify.toml ├── github-pr ├── fork-repo.png ├── diff-read-json.png ├── send-edit-suggestions.png ├── revoke-edit-suggestions.png ├── README.md └── mavo-github-pr.js ├── templates ├── _footer-demo.html ├── _header.html ├── _head-demos.html ├── _footer-docs.html ├── _head-docs.html ├── _head-demo.html ├── _head.html ├── _nav.html ├── _notice.html ├── _docs-index.html └── _footer.html ├── locale-es ├── README.md └── mavo-locale-es.js ├── revert ├── mavo-revert.css └── mavo-revert.js ├── list-separator ├── README.md ├── mavo-list-separator.js └── test.html ├── clear ├── mavo-clear.css ├── README.md ├── mavo-clear.js └── test.html ├── plugin ├── plugin.js ├── index.tpl.html └── index.html ├── microdata ├── README.md ├── mavo-microdata.js └── test.html ├── twitter ├── README.md └── mavo-twitter.js ├── yaml ├── mavo-yaml.js └── test.html ├── debug ├── test.html ├── README.md ├── mavo-debug.scss ├── mavo-debug.css ├── prettyprint.js └── mavo-debug.js ├── gist ├── README.md └── mavo-gist.js ├── locale-el ├── README.md ├── test.html └── mavo-locale-el.js ├── .eslintrc.json ├── tinymce ├── README.md ├── test.html └── mavo-tinymce.js ├── dropbox ├── README.md ├── test.html └── mavo-dropbox.js ├── package.json ├── sitewide.js ├── importhtml ├── mavo-importhtml.js ├── test.html └── README.md ├── gulpfile.js ├── index.tpl.html ├── css ├── style.scss └── style.css ├── markdown ├── README.md ├── test.html └── mavo-markdown.js ├── index.html └── plugins.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.map 3 | -------------------------------------------------------------------------------- /_headers: -------------------------------------------------------------------------------- 1 | /* 2 | Access-Control-Allow-Origin: * 3 | -------------------------------------------------------------------------------- /_redirects: -------------------------------------------------------------------------------- 1 | /plugin/:id /plugin/index.html 200 2 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NODE_VERSION = "14.3" 3 | -------------------------------------------------------------------------------- /github-pr/fork-repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mavoweb/plugins/HEAD/github-pr/fork-repo.png -------------------------------------------------------------------------------- /github-pr/diff-read-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mavoweb/plugins/HEAD/github-pr/diff-read-json.png -------------------------------------------------------------------------------- /templates/_footer-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | @@include('_footer.html') 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /github-pr/send-edit-suggestions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mavoweb/plugins/HEAD/github-pr/send-edit-suggestions.png -------------------------------------------------------------------------------- /github-pr/revoke-edit-suggestions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mavoweb/plugins/HEAD/github-pr/revoke-edit-suggestions.png -------------------------------------------------------------------------------- /locale-es/README.md: -------------------------------------------------------------------------------- 1 | # Spanish localization 2 | 3 | To use, simply include the plugin and then use `lang="es"` on your Mavo root, or the `` element. 4 | -------------------------------------------------------------------------------- /revert/mavo-revert.css: -------------------------------------------------------------------------------- 1 | .mv-bar.mv-ui .mv-revert::before { 2 | content: "✘"; 3 | } 4 | 5 | .mv-bar.mv-ui .mv-revert:enabled:hover { 6 | background: #c50; 7 | } 8 | -------------------------------------------------------------------------------- /templates/_header.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Mavo 4 |

5 | @@include('_nav.html') 6 |
7 | -------------------------------------------------------------------------------- /templates/_head-demos.html: -------------------------------------------------------------------------------- 1 | @@include('_head.html', { 2 | "rootclass": "demos", 3 | "title": "Mavo Demos", 4 | "includes": "" 5 | }) 6 | -------------------------------------------------------------------------------- /templates/_footer-docs.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | @@include('_footer.html') 6 | 7 | 8 | -------------------------------------------------------------------------------- /templates/_head-docs.html: -------------------------------------------------------------------------------- 1 | @@include('_head.html', { 2 | "rootclass": "docs", 3 | "title": "@@title - Mavo Documentation", 4 | "includes": "" 5 | }) 6 | -------------------------------------------------------------------------------- /list-separator/README.md: -------------------------------------------------------------------------------- 1 | Use `mv-list-separator` on the collection (element with `mv-list` or `mv-multiple`). E.g. `mv-list-separator=", "`. Spaces are significant, they are not trimmed. For line breaks, use `mv-list-separator="\n"`. -------------------------------------------------------------------------------- /clear/mavo-clear.css: -------------------------------------------------------------------------------- 1 | .mv-bar.mv-ui .mv-clear::before { 2 | content: var(--mv-rubbish-bin); 3 | width: 1em; 4 | height: 1em; 5 | vertical-align: -.25em; 6 | width: .9em; 7 | filter: saturate(0) brightness(600%); 8 | } 9 | 10 | .mv-bar.mv-ui .mv-clear:enabled:hover { 11 | background: #b00; 12 | } 13 | -------------------------------------------------------------------------------- /plugin/plugin.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | $.events($('[mv-app="plugin"]'), "mv-load", function(evt) { 4 | $$(".github-buttons > a").forEach(function(a) { 5 | a.classList.add("github-button"); 6 | }); 7 | 8 | requestAnimationFrame(function() { 9 | $.include("https://buttons.github.io/buttons.js"); 10 | }); 11 | }); 12 | 13 | })(); 14 | -------------------------------------------------------------------------------- /templates/_head-demo.html: -------------------------------------------------------------------------------- 1 | @@include('_head.html', { 2 | "rootclass": "demos single", 3 | "title": "@@name • Mavo Demos", 4 | "includes": "" 5 | }) 6 | 7 | 8 | 9 | @@include('_header.html') 10 | 11 |

Demos

12 | 13 |
14 |

@@name

15 | -------------------------------------------------------------------------------- /microdata/README.md: -------------------------------------------------------------------------------- 1 | # Import HTML 2 | 3 | This plugin allows you to use Microdata to define Mavo attributes. E.g. `itemprop` instead of `property` and `itemscope`/`itemtype` instead of `mv-group`/`typeof`. This is useful if you were relying on this functionality, since it got removed in Mavo v0.2.0. 4 | 5 | Note that you do **not** need to use this if you’re using a value-less `property` and `itemprop` to provide the property name. That still works out of the box. 6 | -------------------------------------------------------------------------------- /templates/_head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @@title 6 | 7 | 8 | 9 | 10 | 11 | @@includes 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /templates/_nav.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /templates/_notice.html: -------------------------------------------------------------------------------- 1 |

Mavo was released publicly on May 16th, 2017 and is currently in mature beta (gamma? 😝). 2 | This doesn’t mean you can’t use it in production, just make sure to test things well, and if you are going to hand off the site, use a specific Mavo version or local files. 3 | Play around with it and let us know if you encounter any bugs! 4 | Or, if you just like Mavo and want to tell us, we’d love that too! 😊 5 |

6 | -------------------------------------------------------------------------------- /microdata/mavo-microdata.js: -------------------------------------------------------------------------------- 1 | (function($, $$) { 2 | 3 | Mavo.Plugins.register("microdata"); 4 | 5 | Mavo.ready.then(function() { 6 | $$("[mv-app] [itemprop]:not([property])").forEach(function(element) { 7 | element.setAttribute("property", element.getAttribute("itemprop")); 8 | }); 9 | 10 | $$("[mv-app] [itemtype]:not([typeof]):not([mv-group]), [mv-app] [itemscope]:not([typeof]):not([mv-group])").forEach(function(element) { 11 | element.setAttribute("typeof", element.getAttribute("itemtype") || ""); 12 | }); 13 | }); 14 | 15 | })(Bliss, Bliss.$); 16 | -------------------------------------------------------------------------------- /twitter/README.md: -------------------------------------------------------------------------------- 1 | No setup needed, it works automatically on any Markdown properties! 2 | 3 | ###### Demo 4 | 5 | ```html 6 |
7 |
# Heading 8 | 9 | https://twitter.com/mavoweb/status/864474590420008960 10 | 11 | Tweet URLs need to be on their own line to be converted. 12 | See: https://twitter.com/LeaVerou/status/1011006218712834050 13 | 14 | Some *more* tweets… 15 | 16 | https://twitter.com/mavoweb/status/1011698545169240065 17 | https://twitter.com/LeaVerou/status/1011006217248964608 18 |
19 |
20 | ``` -------------------------------------------------------------------------------- /yaml/mavo-yaml.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var _ = Mavo.Formats; 4 | 5 | _.Yaml = $.Class({ 6 | extends: _.Base, 7 | static: { 8 | extensions: [".yaml", ".yml"], 9 | loadOptions: { 10 | json: true 11 | }, 12 | dumpOptions: {}, 13 | dependencies: [{ 14 | test: () => self.jsyaml, 15 | url: "https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.8.3/js-yaml.min.js" 16 | }], 17 | ready: _.Base.ready, 18 | parse: serialized => _.Yaml.ready().then(() => jsyaml.safeLoad(serialized, _.Yaml.loadOptions)), 19 | stringify: data => _.Yaml.ready().then(() => jsyaml.safeDump(data, _.Yaml.dumpOptions)) 20 | } 21 | }); 22 | 23 | Mavo.Plugins.register("yaml"); 24 | 25 | })(); 26 | -------------------------------------------------------------------------------- /twitter/mavo-twitter.js: -------------------------------------------------------------------------------- 1 | (function($, $$) { 2 | 3 | var TwitterWidgets = $.include("https://platform.twitter.com/widgets.js"); 4 | 5 | Mavo.Plugins.register("twitter", { widgets: TwitterWidgets }); 6 | 7 | Mavo.hooks.add("markdown-render-before", function(env) { 8 | if (env.markdown) { 9 | env.markdown = env.markdown.replace(/^\s*https:\/\/twitter.com\/\w{1,50}\/status\/\d+\s*$/gim, function(url) { 10 | return `
${url}
`; 11 | }); 12 | } 13 | }); 14 | 15 | document.addEventListener("mv-markdown-render", function(evt) { 16 | TwitterWidgets.then(function() { 17 | Mavo.Plugins.loaded.twitter.rendered = twttr.widgets.load(evt.target); 18 | }); 19 | }); 20 | 21 | })(Bliss, Bliss.$); 22 | -------------------------------------------------------------------------------- /debug/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mavo Inspector tests 6 | 7 | 8 | 9 | 10 | 11 |

Mavo Inspector tests

12 | 13 |
14 |

Basic

15 | 16 |
17 | 1 18 | YOLO 19 | [prop1] 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /templates/_docs-index.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /clear/README.md: -------------------------------------------------------------------------------- 1 | # Clear toolbar button 2 | 3 | The plugin adds a clear toolbar button as **optional**. 4 | Therefore, after including the plugin, you need to add the clear button to the Mavos you want via `mv-bar="with clear"` or, if you want a custom label, by including it in the HTML, like so: 5 | 6 | ```html 7 |
8 | 9 |
10 | ``` 11 | 12 | Note: Clearing a collection will not empty it, it will return it to the same state it had before any data was stored, i.e. having one item with its data taken from the HTML. 13 | 14 | ## Demo 15 | 16 | ```markup 17 |
19 |

Heading

20 |

Lorem Ipsum

21 |
22 | ``` 23 | -------------------------------------------------------------------------------- /gist/README.md: -------------------------------------------------------------------------------- 1 | # Github Gist 2 | 3 | This plugin allows you to use Github Gists for storage, and even create new ones! 4 | 5 | ## Reading/writing gists 6 | 7 | If you provide a Gist URL, e.g. `mv-storage="https://gist.github.com/username/gistid"` the first file from the gist will be loaded. 8 | You can also provide a specific filename like `mv-storage="https://gist.github.com/username/gistid/filename"` 9 | 10 | If the logged in user is not the same as the gist creator, the gist will be forked upon saving, and the URL will be updated to point to it (without a reload) 11 | 12 | ## Creating gists 13 | 14 | You may also want each user to create their own gist for storing their data. 15 | In that case, you can use `mv-storage="https://gist.github.com"`. 16 | Upon saving, the URL will be updated so that storage points to their specific gist (without a reload). 17 | -------------------------------------------------------------------------------- /locale-el/README.md: -------------------------------------------------------------------------------- 1 | # Greek localization 2 | 3 | To use, simply include the plugin and then use `lang="el"` on your Mavo root, or the `` element. 4 | 5 | ## Demo (Local storage) 6 | 7 | ```markup 8 |
9 | Lea Verou 10 | Χόμπι: Cooking 11 |
12 | ``` 13 | 14 | ## Demo (Github storage) 15 | 16 | If it displays with a compact toolbar (no text), resize the code area and then right click on the example and select "Reload Frame". 17 | 18 | ```markup 19 |
21 | 27 |
28 | ``` 29 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "script", 6 | "ecmaFeatures": { 7 | "impliedStrict": true 8 | } 9 | }, 10 | "env": { 11 | "browser": true, 12 | "node": true 13 | }, 14 | "rules": { 15 | "semi": 1, 16 | "no-dupe-args": 1, 17 | "no-dupe-keys": 1, 18 | "no-unreachable": 1, 19 | "valid-typeof": 1, 20 | "curly": 1, 21 | "no-useless-call": 1, 22 | "brace-style": [1, "stroustrup"], 23 | "linebreak-style": [1, "unix"], 24 | "no-mixed-spaces-and-tabs": [1, "smart-tabs"], 25 | "quotes": [1, "double", "avoid-escape"], 26 | "spaced-comment": [1, "always", { 27 | "block": { 28 | "exceptions": ["*"] 29 | } 30 | }], 31 | "space-before-blocks": [1, "always"], 32 | "arrow-spacing": 1, 33 | "comma-spacing": 1, 34 | "keyword-spacing": 1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tinymce/README.md: -------------------------------------------------------------------------------- 1 | To use TinyMCE, follow the installation instructions below. Then add `class="tinymce"` to the property that you want to be edited via TinyMCE. 2 | 3 | Additionally, you can customize the [tinymce toolbar](https://www.tiny.cloud/docs-4x/configure/editor-appearance/#groupingtoolbarcontrols) that is used for each field. By passing an attribute called `mv-tinymce-toolbar`, you can specify which buttons show on the toolbar, see the [tinymce toolbar docs](https://www.tiny.cloud/docs-4x/advanced/editor-control-identifiers/#toolbarcontrols) for more information. By default the following value is used for the toolbar when nothing is specified: `"styleselect | bold italic | image link | table | bullist numlist"` 4 | 5 | You can also specify additional valid elements using the `mv-tinymce-extended-valid-elements` attribute. This allows you to include elements that TinyMCE might otherwise strip out: `mv-tinymce-extended-valid-elements="span,div[*],p[*]"` 6 | -------------------------------------------------------------------------------- /dropbox/README.md: -------------------------------------------------------------------------------- 1 | # Dropbox 2 | 3 | The [Dropbox](https://dropbox.com) backend provides remote storage, and may be useful for people without a [Github](https://github.com) account. Just like the [Github backend](https://mavo.io/docs/storage#github), it takes care of authentication, and only provides editing and saving controls to users that have appropriate permissions. 4 | 5 | Unlike with Github, Mavo *cannot create the file for you if it does not exist*. To start using it, you need to create an empty file with a `.json` extension, and add it to your Dropbox. Then, in the Dropbox application, click “Share” and copy the link it gives you (you may need to click “Copy Link” to get to it). It will look like `https://www.dropbox.com/s/5fsvey23bi0v8lf/myfile.json?dl=0`. This link is what you will use in the `mv-storage` attribute: 6 | 7 | ```html 8 |
9 | ... 10 |
11 | ``` 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mavo-plugins", 3 | "version": "0.0.3", 4 | "description": "Mavo plugin repository", 5 | "main": "mavo.js", 6 | "scripts": { 7 | "test": "open ." 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/LeaVerou/mavo-plugins.git" 12 | }, 13 | "author": "Lea Verou", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/LeaVerou/mavo-plugins/issues" 17 | }, 18 | "homepage": "http://plugins.mavo.io", 19 | "browserslist": "> 0.19%, not IE <= 11, not samsung <= 4, not op_mini all, not chrome <= 55", 20 | "devDependencies": { 21 | "babel-eslint": "^10.1.0", 22 | "gulp": "^4.0.2", 23 | "gulp-autoprefixer": "^7.0.1", 24 | "gulp-file-include": "^2.2.2", 25 | "gulp-notify": "^3.2.0", 26 | "gulp-rename": "^2.0.0", 27 | "gulp-replace": "^1.0.0", 28 | "gulp-sass": "^4.1.0", 29 | "gulp-sourcemaps": "^2.6.5", 30 | "gulp-util": "^3.0.7" 31 | }, 32 | "dependencies": { 33 | "acorn": "^7.3.1", 34 | "eslint": "^7.3.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /debug/README.md: -------------------------------------------------------------------------------- 1 | # Mavo inspector 2 | 3 | ## How to use 4 | 5 | You enable the Mavo inspector on a Mavo app by ising the `mv-debug` class on its root or adding `?debug` to the URL. After reloading you will see the tools. If you only want to debug storage, use the class `mv-debug-saving` instead of `mv-debug`. 6 | 7 | ## The tools 8 | 9 | On every group, you will get a table with the current values of all properties and expressions. **The expressions in the table are editable** and you can see the results of your edits as you type, both in the table and in your app. 10 | 11 | Hovering over the elements in the last column will highlight them in the app. 12 | 13 | ## Example 14 | 15 | ```html 16 |
17 |

18 | , 19 | 20 |

21 | 28 |
29 | ``` 30 | -------------------------------------------------------------------------------- /dropbox/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dropbox tests 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Dropbox tests

14 | 15 |
16 |

Basic

17 | 18 | 19 | 20 | 28 | 35 | 36 |
21 |
    22 |
  • 23 | Code 24 | Name 25 |
  • 26 |
27 |
29 |
    30 |
  • Online
  • 31 |
  • us United States
  • 32 |
  • gb United Kingdom
  • 33 |
34 |
37 |
38 | 39 |
41 |

Upload

42 | 43 |

Try to upload an image below. Try pasting an image, or drag & drop too.

44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tinymce/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TinyMCE tests 6 | 7 | 8 | 9 | 10 | 11 |

TinyMCE tests

12 | 13 |
14 |

Rich text properties

15 | 16 |
17 | Hello, I am Lea Verou. Mavo is awesome. 18 |
19 | 20 |
21 | And this is another one. 22 |
23 |
24 | 25 | 30 |
31 |

XSS

32 | 33 | 34 | 39 | 44 | 45 |
35 |
36 | XSS 37 |
38 |
40 |
41 | Foo bar 42 |
43 |
46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /locale-el/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Greek locale tests 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Greek locale tests

13 | 14 |
15 |

Mavo with Github storage

16 | 17 | 18 | 19 | 27 | 34 | 35 |
20 |
    21 |
  • 22 | Code 23 | Name 24 |
  • 25 |
26 |
28 |
    29 |
  • Online
  • 30 |
  • us United States
  • 31 |
  • gb United Kingdom
  • 32 |
33 |
36 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /sitewide.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | if (!self.document) { 4 | // We're in a service worker! Oh man, we’re living in the future! 🌈🦄 5 | if (location.hostname === "localhost") { 6 | // We're testing locally, use local URLs for Mavo 7 | self.addEventListener("fetch", function(evt) { 8 | var url = evt.request.url; 9 | 10 | if (url.indexOf("get.mavo.io/mavo.") > -1 || url.indexOf("dev.mavo.io/dist/mavo.") > -1) { 11 | var newURL = url.replace(/.+?(get|dev)\.mavo\.io\/(dist\/)?/, "http://localhost:8000/dist/") + "?" + Date.now(); 12 | } 13 | else if (/plugins.mavo.io\/(\w+)\/(?:mavo-\1.js|mavo-\1.css|README.md)$/.test(url)) { 14 | var newURL = new URL(url); 15 | newURL.host = location.host; 16 | newURL.protocol = location.protocol; 17 | newURL += ""; 18 | } 19 | else if (/\/plugin\/\w+\/?$/.test(url)) { 20 | // Doesn't currently work :( 21 | var newURL = url.replace("/plugin/", "/plugin/?plugin="); 22 | } 23 | 24 | if (newURL) { 25 | var response = fetch(new Request(newURL), evt.request) 26 | .then(r => r.status < 400? r : Promise.reject()) 27 | .catch(err => fetch(evt.request)); // if that fails, return original request 28 | 29 | evt.respondWith(response); 30 | } 31 | }); 32 | } 33 | 34 | return; 35 | } 36 | 37 | var src = document.currentScript ? document.currentScript.src : "sitewide.js"; 38 | 39 | if ("serviceWorker" in navigator) { 40 | // Register this script as a service worker 41 | addEventListener("load", function() { 42 | navigator.serviceWorker.register(src); 43 | }); 44 | } 45 | 46 | 47 | })(); 48 | -------------------------------------------------------------------------------- /importhtml/mavo-importhtml.js: -------------------------------------------------------------------------------- 1 | (function($, $$) { 2 | 3 | Mavo.Plugins.register("importhtml", { 4 | ready: $.ready(function() { 5 | var properties = { 6 | "--mv-property": "property", 7 | "--mv-typeof": "typeof", 8 | "--mv-datatype": "datatype" 9 | }; 10 | 11 | Mavo.attributes.forEach(name => { 12 | properties["--" + name] = name; 13 | }); 14 | 15 | delete properties["--mv-plugins"]; 16 | 17 | var propertyNames = Object.keys(properties); 18 | 19 | var selectors = []; 20 | 21 | for (var stylesheet of document.styleSheets) { 22 | try { 23 | var rules = stylesheet.cssRules; 24 | } 25 | catch (e) {} 26 | 27 | if (rules) { 28 | for (var rule of rules) { 29 | if (rule && rule.style && Mavo.Functions.intersects(propertyNames, Array.from(rule.style))) { 30 | selectors.push(rule.selectorText); 31 | } 32 | } 33 | } 34 | } 35 | 36 | if (selectors.length) { 37 | $.create("style", { 38 | textContent: `* { 39 | ${propertyNames.map(name => `${name}: initial;`).join("\n\t")} 40 | }`, 41 | inside: document.head 42 | }); 43 | 44 | $$(selectors.join(", ")).forEach(function(element) { 45 | var cs = getComputedStyle(element); 46 | 47 | for (var property in properties) { 48 | var value = cs.getPropertyValue(property).trim(); 49 | var attribute = properties[property]; 50 | 51 | if (value && !element.hasAttribute(attribute)) { 52 | value = value === "true"? "" : value; 53 | element.setAttribute(attribute, value); 54 | } 55 | } 56 | }); 57 | } 58 | }) 59 | }); 60 | 61 | })(Bliss, Bliss.$); 62 | -------------------------------------------------------------------------------- /clear/mavo-clear.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | Mavo.Plugins.register("clear", { 4 | dependencies: [ 5 | "mavo-clear.css" 6 | ], 7 | extend: { 8 | Mavo: { 9 | clear: function() { 10 | if (confirm(this._("delete-confirmation"))) { 11 | this.store(null).then(() => this.root.clear()); 12 | } 13 | } 14 | }, 15 | Node: { 16 | clear: function() { 17 | this.propagate("clear"); 18 | } 19 | }, 20 | Group: { 21 | clear: function() { 22 | // Delete invisible properties 23 | let hasInvisible = false; 24 | 25 | for (let prop in this.data) { 26 | if (!(prop in this.children)) { 27 | hasInvisible = true; 28 | delete this.data[prop]; 29 | delete this.liveData.data[prop]; 30 | } 31 | } 32 | 33 | this.propagate("clear"); 34 | 35 | if (hasInvisible) { 36 | this.dataChanged("clear"); 37 | } 38 | } 39 | }, 40 | Collection: { 41 | /** 42 | * Delete all items in the collection. Not undoable. 43 | */ 44 | clear: function() { 45 | this.render([]); 46 | 47 | if (this.initializeData) { 48 | this.initializeData(); 49 | } 50 | 51 | this.propagate("clear"); 52 | } 53 | }, 54 | Primitive: { 55 | clear: function() { 56 | this.render(this.initialValue); 57 | } 58 | } 59 | } 60 | }); 61 | 62 | Mavo.UI.Bar.controls.clear = { 63 | action: function() { 64 | this.clear(); 65 | }, 66 | permission: "delete", 67 | optional: true 68 | }; 69 | 70 | Mavo.Locale.register("en", { 71 | "clear": "Clear", 72 | "delete-confirmation": "This will delete all your data. Are you sure?" 73 | }); 74 | 75 | })(); 76 | -------------------------------------------------------------------------------- /list-separator/mavo-list-separator.js: -------------------------------------------------------------------------------- 1 | Mavo.Plugins.register("list-separator", { 2 | hooks: { 3 | "collection-init-end": function() { 4 | this.separator = this.element.getAttribute("mv-list-separator"); 5 | 6 | if (["\\n", "\\r", "\\r\\n"].includes(this.separator)) { 7 | // Handle line breaks specially, parse any line break, store with detected line break 8 | this.separator = /\r?\n/g; 9 | this.joinSeparator = "\n"; // Default line break to join with 10 | } 11 | 12 | // TODO ignore separator if this is not a collection of primitives and print error 13 | }, 14 | "node-getdata-end": function(env) { 15 | if (this instanceof Mavo.Collection && this.separator) { 16 | // Escape separator in data 17 | let data = env.data.map(s => (s + "").replaceAll(this.separator, "\\$&")); 18 | env.data = data.join(this.joinSeparator ?? this.separator); 19 | } 20 | }, 21 | "node-render-start": function(env) { 22 | if (this instanceof Mavo.Collection && this.separator && env.data.split) { 23 | let separatorString = this.separator.source ?? this.separator; 24 | let separator = RegExp(`(? 1) { 28 | // If the separator is a regexp, we need to store the first occurrence and use it to join when saving 29 | this.joinSeparator = env.data.match(separator)[0]; 30 | } 31 | 32 | // Unescape separator in the data 33 | data = data.map(s => s.replace(RegExp(`\\\\(?=${separatorString})`, "g"), "")); 34 | env.data = data; 35 | } 36 | } 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | Build file to concat & minify files, compile SCSS and so on. 3 | npm install gulp gulp-util gulp-uglify gulp-rename gulp-concat gulp-sourcemaps gulp-babel gulp-sass gulp-autoprefixer --save-dev 4 | */ 5 | // grab our gulp packages 6 | var gulp = require("gulp"); 7 | var rename = require("gulp-rename"); 8 | var sass = require("gulp-sass"); 9 | var autoprefixer = require("gulp-autoprefixer"); 10 | var sourcemaps = require("gulp-sourcemaps"); 11 | var notify = require("gulp-notify"); 12 | var fileinclude = require("gulp-file-include"); 13 | 14 | gulp.task("sass", function () { 15 | return gulp.src(["**/*.scss", "!node_modules/**"]) 16 | .pipe(sourcemaps.init()) 17 | .pipe(sass().on("error", sass.logError)) 18 | .pipe(autoprefixer({ 19 | cascade: false 20 | })) 21 | .pipe(rename({ extname: ".css" })) 22 | .pipe(sourcemaps.write(".")) 23 | .pipe(gulp.dest(".")) 24 | .pipe(notify({ 25 | message: "Sass done!", 26 | onLast: true 27 | })); 28 | }); 29 | 30 | gulp.task("update", function() { 31 | gulp.src(["../mavo/.eslintrc.json"]).pipe(gulp.dest(".")); 32 | return gulp.src(["../mavo.io/templates/*.html"]).pipe(gulp.dest("templates")); 33 | }); 34 | 35 | gulp.task("html", function() { 36 | return gulp.src(["**/*.tpl.html"]) 37 | .pipe(fileinclude({ 38 | basepath: "templates/", 39 | context: { 40 | webRoot: "//mavo.io" 41 | } 42 | }).on("error", function(error) { 43 | console.error(error); 44 | })) 45 | .pipe(rename({ extname: "" })) 46 | .pipe(rename({ extname: ".html" })) 47 | .pipe(gulp.dest(".")) 48 | .pipe(notify({ 49 | message: "HTML done!", 50 | onLast: true 51 | })); 52 | }); 53 | 54 | gulp.task("watch", function() { 55 | gulp.watch(["../mavo/.eslintrc.json"], gulp.series("update")); 56 | gulp.watch(["**/*.scss"], gulp.series("sass")); 57 | gulp.watch(["**/*.tpl.html", "../mavo.io/templates/*.html"], gulp.series("html")); 58 | }); 59 | 60 | gulp.task("default", gulp.parallel("sass", "html")); 61 | -------------------------------------------------------------------------------- /revert/mavo-revert.js: -------------------------------------------------------------------------------- 1 | (function ($, $$) { 2 | 3 | Mavo.Plugins.register("revert", { 4 | dependencies: [ 5 | "mavo-revert.css" 6 | ], 7 | extend: { 8 | Mavo: { 9 | revert: function() { 10 | this.root.revert(); 11 | }, 12 | }, 13 | Node: { 14 | revert: function() { 15 | this.propagate("revert"); 16 | } 17 | }, 18 | Collection: { 19 | revert: function() { 20 | for (let item of this.children) { 21 | // Delete added items 22 | if (item.unsavedChanges) { 23 | this.delete(item, true); 24 | } 25 | else { 26 | // Bring back deleted items 27 | if (item.deleted) { 28 | item.deleted = false; 29 | } 30 | 31 | // Revert all properties 32 | item.revert(); 33 | } 34 | } 35 | }, 36 | }, 37 | Primitive: { 38 | revert: function() { 39 | if (this.unsavedChanges && this.savedValue !== undefined) { 40 | // FIXME if we have a collection of properties (not groups), this will cause 41 | // cancel to not remove new unsaved items 42 | // This should be fixed by handling this on the collection level. 43 | this.value = this.savedValue; 44 | this.unsavedChanges = false; 45 | } 46 | }, 47 | } 48 | }, 49 | 50 | hooks: { 51 | "init-end": function() { 52 | this.permissions.can("save", () => { 53 | if (!this.autoSave) { 54 | // Revert is pointless if autosaving, there's not enough time between saves to click it 55 | this.ui.revert = $.create("button", { 56 | className: "mv-revert", 57 | textContent: "Revert", 58 | title: "Revert", 59 | disabled: true, 60 | inside: this.ui.bar 61 | }); 62 | } 63 | }, () => { 64 | $.remove(this.ui.revert); 65 | this.ui.revert = null; 66 | }); 67 | 68 | $.delegate(this.element, "click", { 69 | ".mv-revert": evt => { 70 | if (this.permissions.save) { 71 | this.revert(); 72 | } 73 | } 74 | }); 75 | } 76 | } 77 | }); 78 | 79 | })(Bliss, Bliss.$); 80 | -------------------------------------------------------------------------------- /index.tpl.html: -------------------------------------------------------------------------------- 1 | @@include('_head.html', { 2 | "rootclass": "plugins", 3 | "title": "Mavo Plugins", 4 | "includes": "" 5 | }) 6 | 7 | 8 | 9 | @@include('_header.html') 10 | 11 | 12 |

Plugins

13 | 14 |
15 |

Plugins teach Mavo new tricks, so you can create even more awesomeness!

16 | 17 |
18 | 19 |

20 | Some plugins are hidden. Show All. 21 |

22 |
23 | 44 |
45 | 46 |
47 |

48 | 49 |

50 |
51 |
52 | 53 |
54 | 55 | @@include('_footer.html') 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /importhtml/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Import HTML tests 6 | 7 | 8 | 9 | 10 | 24 | 25 | 26 | 27 |

Import HTML tests

28 | 29 | 30 | 31 | 32 | 35 | 61 | 62 | 63 | 85 | 88 | 89 |
33 |

34 | 		
36 |
{
37 | 	"collection": [
38 | 		{
39 | 			"title": "Heading1",
40 | 			"content": "Content1"
41 | 		},
42 | 		{
43 | 			"title": "Heading2",
44 | 			"content": "Content2"
45 | 		},
46 | 		{
47 | 			"title": "Heading3",
48 | 			"content": "Content3"
49 | 		},
50 | 		{
51 | 			"title": "Heading4",
52 | 			"content": "Content4"
53 | 		},
54 | 		{
55 | 			"title": "Heading5",
56 | 			"content": "Content5"
57 | 		}
58 | 	]
59 | }
60 |
64 |
65 |

Heading1

66 |

Content1

67 |
68 |
69 |

Heading2

70 |

Content2

71 |
72 |
73 |

Heading3

74 |

Content3

75 |
76 |
77 |

Heading4

78 |

Content4

79 |
80 |
81 |

Heading5

82 |

Content5

83 |
84 |
86 | article[property=collection][mv-multiple] > h1[property=title]:not([mv-multiple]) + p[property=content]:not([mv-multiple]) 87 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /templates/_footer.html: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 39 | 40 | 50 | -------------------------------------------------------------------------------- /locale-el/mavo-locale-el.js: -------------------------------------------------------------------------------- 1 | Mavo.Locale.register("el", { 2 | "second": "δευτερόλεπτο", 3 | "seconds": "δευτερόλεπτα", 4 | "minute": "λεπτό", 5 | "minutes": "λεπτά", 6 | "hour": "ώρα", 7 | "hours": "ώρες", 8 | "day": "μέρα", 9 | "days": "μέρες", 10 | "week": "εβδομάδα", 11 | "weeks": "εβδομάδες", 12 | "month": "μήνας", 13 | "months": "μήνες", 14 | "year": "χρόνος", 15 | "years": "χρόνια", 16 | "edit": "Επεξεργασία", 17 | "save": "Αποθήκευση", 18 | "clear": "Καθαρισμός", 19 | "logout": "Αποσύνδεση", 20 | "login": "Σύνδεση", 21 | "logged-in-as": "Συνδεδεμένος/η στο {id} ως", 22 | "login-to": "Σύνδεση στο {id}", 23 | "loading": "Φόρτωση", 24 | "uploading": "Μεταφόρτωση", 25 | "saving": "Αποθήκευση", 26 | "error-uploading": "Σφάλμα κατά τη φόρτωση του αρχείου", 27 | "problem-saving": "Πρόβλημα κατά την αποθήκευση δεδομένων", 28 | "http-error": "Σφάλμα HTTP {status}: {statusText}", 29 | "cant-connect": "Αδυναμία σύνδεσης με το διαδίκτυο", 30 | "add-item": "Προσθήκη {name}", 31 | "add-item-before": "Προσθήκη νέου {name} πριν", 32 | "add-item-after": "Προσθήκη νέου {name} μετά", 33 | "drag-to-reorder": "Σύρετε για να αλλάξετε τη σειρά του {name}", 34 | "delete-item": "Διαγραφή αυτού του {name}", 35 | "delete-confirmation": "Αυτή η ενέργεια θα διαγράψει όλα σας τα δεδομένα. Είστε βέβαιοι ότι θέλετε να συνεχίσετε;", 36 | "gh-edit-suggestion-saved-in-profile": "Οι αλλαγές σας έχουν αποθηκευθεί στο δικό σας προφίλ, επειδή δεν έχετε δικαίωμα επεξεργασίας σε αυτή τη σελίδα.", 37 | "gh-edit-suggestion-instructions": "Γράψτε μια σύντομη περιγραφή των αλλαγών σας παρακάτω για να τις προτείνετε στους διαχειριστές:", 38 | "gh-edit-suggestion-notreviewed": "Έχετε επιλέξει να προτείνετε τις αλλαγές σας στους διαχειριστές. Οι αλλαγές σας δεν έχουν κρι Your suggestions have not been reviewed yet.", 39 | "gh-edit-suggestion-send": "Αποστολή πρότασης αλλαγών", 40 | "gh-edit-suggestion-revoke": "Ακύρωση πρότασης αλλαγών", 41 | "gh-edit-suggestion-reason-placeholder": "Πρόσθεσα / Διόρθωσα / Διέγραψα ...", 42 | "gh-edit-suggestion-cancelled": "Η πρόταση αλλαγών ακυρώθηκε επιτυχώς!", 43 | "gh-edit-suggestion-title": "Προτεινόμενες αλλαγές στα δεδομένα", 44 | "gh-edit-suggestion-body": `Καλησπέρα! Χρησιμοποίησα το Mavo για να προτείνω τις ακόλουθες αλλαγές: 45 | {description} 46 | Προεπισκόπησε τις αλλαγές μου εδώ: {previewURL}`, 47 | "gh-edit-suggestion-sent": "Η πρόταση αλλαγών εστάλη επιτυχώς!", 48 | "gh-updated-file": "Ενημέρωση του {name}" 49 | }); 50 | -------------------------------------------------------------------------------- /microdata/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Microdata tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Microdata tests

14 | 15 | 16 | 17 | 18 | 21 | 52 | 53 | 54 | 80 | 86 | 93 | 94 |
19 |

 20 | 		
22 |
{
 23 | 	"col": [
 24 | 		{
 25 | 			"name": "Heading1",
 26 | 			"description": "Content1",
 27 | 			"@type": "http://schema.org/Person"
 28 | 		},
 29 | 		{
 30 | 			"name": "Heading2",
 31 | 			"description": "Content2",
 32 | 			"@type": "http://schema.org/Person"
 33 | 		},
 34 | 		{
 35 | 			"name": "Heading3",
 36 | 			"description": "Content3",
 37 | 			"@type": "http://schema.org/Person"
 38 | 		},
 39 | 		{
 40 | 			"name": "Heading4",
 41 | 			"description": "Content4",
 42 | 			"@type": "http://schema.org/Person"
 43 | 		},
 44 | 		{
 45 | 			"name": "Heading5",
 46 | 			"description": "Content5",
 47 | 			"@type": "http://schema.org/Person"
 48 | 		}
 49 | 	]
 50 | }
51 |
55 |
{
 56 | 	"col": [
 57 | 		{
 58 | 			"name": "Heading1",
 59 | 			"description": "Content1"
 60 | 		},
 61 | 		{
 62 | 			"name": "Heading2",
 63 | 			"description": "Content2"
 64 | 		},
 65 | 		{
 66 | 			"name": "Heading3",
 67 | 			"description": "Content3"
 68 | 		},
 69 | 		{
 70 | 			"name": "Heading4",
 71 | 			"description": "Content4"
 72 | 		},
 73 | 		{
 74 | 			"name": "Heading5",
 75 | 			"description": "Content5"
 76 | 		}
 77 | 	]
 78 | }
79 |
81 |
82 |

Heading1

83 |

Content1

84 |
85 |
87 |
Heading1 Content1
88 |
Heading2 Content2
89 |
Heading3 Content3
90 |
Heading4 Content4
91 |
Heading5 Content5
92 |
95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /yaml/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yaml tests 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Yaml tests

13 | 14 |
15 |

JSON to Yaml

16 | 17 |

Press Save in both Mavos.

18 | 19 | 20 | 21 | 24 | 46 | 47 | 48 | 51 | 64 | 65 | 66 | 75 | 81 | 82 | 83 | 92 | 98 | 99 |
22 |

 23 | 			
25 |
{
 26 | 	"group": {
 27 | 		"primitives": [
 28 | 			"foo",
 29 | 			"bar",
 30 | 			"3",
 31 | 			"true"
 32 | 		]
 33 | 	},
 34 | 	"objects": [
 35 | 		{
 36 | 			"number": 1,
 37 | 			"bool": true
 38 | 		},
 39 | 		{
 40 | 			"number": 2,
 41 | 			"bool": false
 42 | 		}
 43 | 	]
 44 | }
45 |
49 |

 50 | 			
52 |
group:
 53 |   primitives:
 54 |     - foo
 55 |     - bar
 56 |     - '3'
 57 |     - 'true'
 58 | objects:
 59 |   - number: 1
 60 |     bool: true
 61 |   - number: 2
 62 |     bool: false
63 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
76 | foo
bar
3
true 77 | 1 78 | true 79 | 2 80 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
93 | foo
bar
3
true 94 | 1 95 | true 96 | 2 97 |
100 |
101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /locale-es/mavo-locale-es.js: -------------------------------------------------------------------------------- 1 | Mavo.Locale.register("es", { 2 | "second": "segundo", 3 | "seconds": "segundos", 4 | "minute": "minuto", 5 | "minutes": "minutos", 6 | "hour": "hora", 7 | "hours": "horas", 8 | "day": "día", 9 | "days": "días", 10 | "week": "semana", 11 | "weeks": "semanas", 12 | "month": "mes", 13 | "months": "meses", 14 | "year": "año", 15 | "years": "años", 16 | "edit": "Editar", 17 | "editing": "Editando", 18 | "save": "Guardar", 19 | "import": "Importar", 20 | "export": "Exportar", 21 | "logout": "Déconnexion", 22 | "login": "Connexion", 23 | "loading": "Cargando", 24 | "uploading": "Subiendo", 25 | "saving": "Guardando", 26 | "dismiss": "Descartar", 27 | "logged-in-as": "Conectado en {id} como ", 28 | "login-to": "Conectarse como {id}", 29 | "error-uploading": "Error subiendo archivo", 30 | "cannot-load-uploaded-file": "No se pudo cargar el archivo", 31 | "filename": "¿Archivo?", 32 | "problem-saving": "Problema guardando datos", 33 | "problem-loading": "Problema cargando datos", 34 | "cannot-parse": "No logro compreder este archivo", 35 | "http-error": "Error HTTP{status}: {statusText}", 36 | "cant-connect": "No logro conectarme a Internet", 37 | "add-item": "Agregar {name}", 38 | "add-item-before": "Agregar {name} después de", 39 | "add-item-after": "Agregar {name} antes de", 40 | "drag-to-reorder": "Reordenar {name}", 41 | "delete-item": "Borrar este {name}", 42 | "item-deleted": "{name} borrado", 43 | "n-items": "{n} {name} elementos", 44 | "undo": "Anular", 45 | "gh-updated-file": "Actualizado {name}", 46 | "gh-edit-suggestion-saved-in-profile": 'Sus cambios han sido enregistrados en su cuenta, dado que Usted no está autorizado a editar esta página.', 47 | "gh-edit-suggestion-instructions": "Escriba una corta descripción de los cambios aportados para proponerlos a los administradores:", 48 | "gh-edit-suggestion-notreviewed": "Usted ha propuesto sus modificaciones a los administradores, pero estas no han sido vistas aún.", 49 | "gh-edit-suggestion-send": "Enviar sugestión", 50 | "gh-edit-suggestion-revoke": "Anular sugestión", 51 | "gh-edit-suggestion-reason-placeholder": "He añadido / corregido / borrado…", 52 | "gh-edit-suggestion-cancelled": "Su propocisión ha sido anulada", 53 | "gh-edit-suggestion-title": "Proponer modificaciones", 54 | "gh-edit-suggestion-body": `¡Hola! He usado Mavo para proponer las siguientes modificaciones: 55 | {description} 56 | Vean mis sugestiones aquí: {previewURL}`, 57 | "gh-edit-suggestion-sent": "Sus proposiciones han sido enviadas", 58 | "gh-login-fork-options": "Usted tiene su propia copia de esta página. ¿Desearía utilizarla?", 59 | "gh-use-my-fork": "Sí. Muéstreme los datos." 60 | }); 61 | -------------------------------------------------------------------------------- /dropbox/mavo-dropbox.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | Mavo.Plugins.register("dropbox"); 4 | 5 | let _ = Mavo.Backend.register(class Dropbox extends Mavo.Backend { 6 | id = "Dropbox" 7 | 8 | constructor (url, o) { 9 | super(url, o); 10 | 11 | this.permissions.on(["login", "read"]); 12 | 13 | this.login(true); 14 | } 15 | 16 | update (url, o) { 17 | super.update(url, o); 18 | 19 | this.url = _.fixShareURL(this.url); 20 | } 21 | 22 | async upload (file, path) { 23 | path = this.path.replace(/[^/]+$/, "") + path; 24 | 25 | await this.put(file, path); 26 | return this.getURL(path); 27 | } 28 | 29 | async getURL (path) { 30 | let shareInfo = await this.request("sharing/create_shared_link_with_settings", {path}, "POST"); 31 | return _.fixShareURL(shareInfo.url); 32 | } 33 | 34 | /** 35 | * Saves a file to the backend. 36 | * @param {Object} file - An object with name & data keys 37 | * @return {Promise} A promise that resolves when the file is saved. 38 | */ 39 | put (serialized, path = this.path, o = {}) { 40 | return this.request("https://content.dropboxapi.com/2/files/upload", serialized, "POST", { 41 | headers: { 42 | "Dropbox-API-Arg": JSON.stringify({ 43 | path, 44 | mode: "overwrite" 45 | }), 46 | "Content-Type": "application/octet-stream" 47 | } 48 | }); 49 | } 50 | 51 | oAuthParams = () => `&redirect_uri=${encodeURIComponent("https://auth.mavo.io")}&response_type=code` 52 | 53 | async getUser () { 54 | if (this.user) { 55 | return this.user; 56 | } 57 | 58 | let info = await this.request("users/get_current_account", "null", "POST"); 59 | 60 | this.user = { 61 | username: info.email, 62 | name: info.name.display_name, 63 | avatar: info.profile_photo_url, 64 | info 65 | }; 66 | 67 | $.fire(this, "mv-login"); 68 | } 69 | 70 | async login (passive) { 71 | await this.oAuthenticate(passive); 72 | await this.getUser(); 73 | 74 | if (this.user) { 75 | this.permissions.logout = true; 76 | 77 | // Check if can actually edit the file 78 | let info = await this.request("sharing/get_shared_link_metadata", { 79 | "url": this.source 80 | }, "POST"); 81 | 82 | this.path = info.path_lower; 83 | this.permissions.on(["edit", "save"]); 84 | } 85 | } 86 | 87 | logout () { 88 | return this.oAuthLogout(); 89 | } 90 | 91 | static apiDomain = "https://api.dropboxapi.com/2/" 92 | static oAuth = "https://www.dropbox.com/oauth2/authorize" 93 | static key = "2mx6061p054bpbp" 94 | 95 | static test (url) { 96 | url = new URL(url, Mavo.base); 97 | return /dropbox.com/.test(url.host); 98 | } 99 | 100 | // Transform the dropbox shared URL into something raw and CORS-enabled 101 | static fixShareURL = url => { 102 | url = new URL(url, Mavo.base); 103 | url.hostname = "dl.dropboxusercontent.com"; 104 | url.search = url.search.replace(/\bdl=0|^$/, "raw=1"); 105 | return url; 106 | } 107 | }); 108 | 109 | })(Bliss); 110 | -------------------------------------------------------------------------------- /clear/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Clear tests 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Clear tests

13 | 14 |
15 |

Basic

16 | 17 |

Press Clear and Save

18 | 19 | 20 | 25 | 28 | 29 | 30 | 35 | 38 | 39 | 40 | 47 | 50 | 51 | 52 | 60 | 63 | 64 | 65 | 75 | 79 | 80 | 81 | 85 | 88 | 89 |
21 | 42 22 | [primitive] 23 | 24 | 26 | 42 42 27 |
31 | 42 32 | [primitive] 33 | 34 | 36 | 6 6 37 |
41 |
42 | 42 43 |
44 | [sum(primitives)] 45 | 46 |
48 | 42 42 49 |
53 |
54 | 9 55 | 56 |
57 | [yolo.foo] 58 | 59 |
61 | 9 10 9 62 |
66 |
67 |
68 | 1 69 | 70 |
71 |
72 | [count(item)] 73 | 74 |
76 | 1 2 77 | 1 78 |
82 | [invisible] 83 | 84 | 86 | 87 |
90 |
 91 | 		{
 92 | 			"primitive": 5,
 93 | 			"primitives": [1, 2, 3],
 94 | 			"item": [{"foo": 3, "bar": 4}, {"foo": 5, "bar": 6}],
 95 | 			"yolo": {"foo": 7, "bar": 8},
 96 | 			"invisible": 60
 97 | 		}
 98 | 	
99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /list-separator/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | List separator tests 6 | 7 | 8 | 9 | 18 | 19 | 20 | 21 |

List separator tests

22 |
 23 | {
 24 | 	"foo": ["1, 4", "2", "3"]
 25 | }
 26 | 
27 | 28 |
29 |

Basic

30 | 31 | 32 | 33 | 38 | 41 | 42 | 43 | 46 | 51 | 52 | 53 | 59 | 62 | 63 | 64 | 67 | 72 | 73 |
34 |
35 | 36 |
37 |
39 | 1, 423 40 |
44 |

 45 | 			
47 |
{
 48 | 	"foo": "1\\, 4, 2, 3"
 49 | }
50 |
54 |
55 | 56 |
57 | 58 |
60 | 1, 4235 61 |
65 |

 66 | 			
68 |
{
 69 | 	"foo": "1\\, 4, 2, 3, 5"
 70 | }
71 |
74 |
75 | 76 |
77 |

Line breaks

78 | 79 |
{
 80 | 		"foo": ["1\n4", "2", "3"]
 81 | 	}
82 | 83 | 84 | 85 | 90 | 94 | 95 | 96 | 99 | 104 | 105 | 106 | 112 | 116 | 117 | 118 | 121 | 126 | 127 |
86 |
87 | 88 |
89 |
91 | 1 92 | 423 93 |
97 |

 98 | 			
100 |
{
101 | 	"foo": "1\\\n4\n2\n3"
102 | }
103 |
107 |
108 | 109 |
110 | 111 |
113 | 1 114 | 4235 115 |
119 |

120 | 			
122 |
{
123 | 	"foo": "1\\\n4\n2\n3\n5"
124 | }
125 |
128 |
129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /css/style.scss: -------------------------------------------------------------------------------- 1 | body > header { 2 | .logo a { 3 | white-space: nowrap; 4 | 5 | img { 6 | display: inline-block; 7 | } 8 | 9 | &::after { 10 | content: "+"; 11 | display: inline-block; 12 | vertical-align: .25em; 13 | color: hsla(0,0%,100%, .6); 14 | } 15 | } 16 | } 17 | 18 | body > header + h2 { 19 | background: var(--color-blue); 20 | } 21 | 22 | .intro { 23 | font-size: 200%; 24 | } 25 | 26 | section[mv-app="plugins"] { 27 | & > div { 28 | display: flex; 29 | flex-flow: row wrap; 30 | justify-content: space-between; 31 | } 32 | } 33 | 34 | article[property="plugin"] { // Plugin listing 35 | width: 100%; 36 | margin: .5rem 0; 37 | 38 | @media (min-width: 950px) { 39 | width: 46%; 40 | } 41 | } 42 | 43 | .plugin { 44 | display: flex; 45 | flex-flow: column; 46 | vertical-align: top; 47 | padding: .75em 1rem; 48 | border: 1px solid hsla(220,10%,0%,.15); 49 | border-radius: .2em; 50 | 51 | & > div { 52 | flex: 1; 53 | } 54 | 55 | h1 { 56 | margin: 0; 57 | font-size: 160%; 58 | font-weight: 300; 59 | font-family: var(--font-body); 60 | 61 | a { 62 | display: flex; 63 | color: var(--color-magenta); 64 | font-weight: inherit; 65 | } 66 | 67 | [property="name"] { 68 | flex: 1; 69 | } 70 | 71 | [property="id"] { 72 | padding: .5em .8em; 73 | border-radius: 1em; 74 | margin: auto; 75 | background: hsl(330, 100%, 95%); 76 | font-size: 50%; 77 | line-height: 1; 78 | font-weight: bold; 79 | } 80 | 81 | a:hover [property="id"] { 82 | background: var(--color-magenta); 83 | color: white; 84 | text-decoration: none; 85 | } 86 | } 87 | 88 | footer { 89 | display: flex; 90 | padding: inherit; 91 | margin: .6em -1rem -.75em; 92 | background: hsla(220,10%,0%,.05); 93 | 94 | & > div { 95 | display: flex; 96 | align-items: center; 97 | } 98 | 99 | .author { 100 | flex: 1; 101 | } 102 | 103 | .button, .github-button, .github-buttons > a { 104 | padding: .35em .8em; 105 | border-radius: .3em; 106 | border: 1px solid rgba(0,0,0,.15); 107 | background: linear-gradient(white, transparent); 108 | font: 600 12px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; // same as GH button 109 | color: inherit; 110 | 111 | &:hover { 112 | background: var(--color-magenta); 113 | color: white; 114 | text-decoration: none; 115 | } 116 | } 117 | 118 | .button, 119 | .github-button, 120 | .github-buttons > span, 121 | iframe { 122 | margin: 0 .1em; 123 | } 124 | 125 | a { 126 | font-weight: bold; 127 | color: var(--color-magenta) 128 | } 129 | } 130 | 131 | &[mv-app] { // View page 132 | font-size: 140%; 133 | 134 | &[mv-progress]::after { 135 | font-size: calc(100% / 1.4); 136 | } 137 | } 138 | } 139 | 140 | a.author { 141 | img { 142 | display: inline-block; 143 | vertical-align: text-top; 144 | max-height: 1.2em; 145 | border-radius: .1em; 146 | } 147 | } 148 | 149 | a[property="tag"] { 150 | display: inline-block; 151 | padding: .1em .4em; 152 | margin-right: .3em; 153 | border-radius: .3em; 154 | background: var(--color-orange); 155 | color: white; 156 | font-weight: bold; 157 | text-transform: uppercase; 158 | font-size: 75%; 159 | 160 | &:hover, &:focus { 161 | text-decoration: none; 162 | background: var(--color-blue); 163 | } 164 | } 165 | 166 | [mv-app="pluginreadme"] { 167 | h1 { 168 | color: var(--color-orange); 169 | font-size: 200%; 170 | } 171 | 172 | h2 { 173 | color: var(--color-magenta); 174 | font-size: 150%; 175 | } 176 | } 177 | 178 | iframe { 179 | border: none; 180 | width: 100%; 181 | } 182 | -------------------------------------------------------------------------------- /markdown/README.md: -------------------------------------------------------------------------------- 1 | # Mavo Markdown 2 | 3 | To use, either give a class of `markdown` to the property you want to enable Markdown on, or specify a Markdown file as your data. Both of these are used in this page, so you can View Source for another example. 4 | 5 | ## Demo 6 | 7 | ```markup 8 |
10 |
### Heading 11 | **This is bold** *This is italic* 12 | Here’s a [link!](https://mavo.io) 13 | 14 | And some code `foo.bar();` 15 |
16 |
17 | ``` 18 | 19 | ## Keyboard shortcuts 20 | 21 | This plugin supports some basic keyboard shortcuts: 22 | 23 | Mac | Windows | Result | 24 | -----|---------|--------| 25 | ⌘ + B|CTRL + B |Make highlighted text **bold**.| 26 | ⌘ + I|CTRL + I |Make highlighted text *italic*.| 27 | ⌘ + E|CTRL + E |Mark highlighted text as a block of `code`.| 28 | ⌘ + K|CTRL + K |Convert highlighted text into a link or insert a placeholder for a link. In either case, you should provide a URL.| 29 | 30 | ## Image upload 31 | 32 | If you are using one of the backends that support uploads such as Github, Dropbox, or Firebase, you can paste an image *from the clipboard* right into the text. The plugin will upload it automatically. 33 | 34 | The plugin also supports the `mv-upload-path` and `mv-upload-url` attributes. For more information, see the [Uploads](https://mavo.io/docs/storage#uploads) section of the Mavo docs. 35 | 36 | ## Customize conversion to HTML 37 | 38 | Showdown supports [a number of options for customizing the way it converts Markdown to HTML](https://github.com/showdownjs/showdown#valid-options). You can specify these options on a per-property basis by using the `mv-markdown-options` attribute. 39 | The syntax of this attribute is a CSS-like list of declarations, where you can use either commas or semicolons to separate the option-value pairs. If you just want to set an option to `true`, you can just provide no value. 40 | Also, if you use this attribute, you can omit the `markdown` class from your element, it's not needed. 41 | 42 | 43 | 44 | ```markup 45 |
46 |
48 | # Heading 49 | 50 | - [x] This task is done 51 | - [ ] This is still pending 52 |
53 |
54 | ``` 55 | 56 |

Advanced customization: Events and Hooks

57 | 58 | If you wish to modify the HTML produced, this plugin fires an `mv-markdown-render` event on the property element that is markdown-enabled right after it converts the Markdown to HTML and renders it. The current Plugins directory uses this for the live demos, you can find the [code here](https://github.com/mavoweb/plugins/blob/master/plugin/plugin.js#L9). 59 | 60 | To further modify how Markdown is converted to HTML, this plugin adds three [hooks](https://mavo.io/docs/plugins/#hooks): 61 | 62 | Name | Details | 63 | ----------|------------------ 64 | `markdown-editor-create` | Allows you to modify the textarea for editing (via `env.editor`) 65 | `markdown-render-before` | Called before converting Markdown to HTML. Allows you to modify the Markdown via `env.markdown`. This plugin directory uses this for note and tip paragraphs ([code](https://github.com/mavoweb/plugins/blob/master/plugin/plugin.js#L5)). 66 | `markdown-render-after` | Called after converting to Markdown to HTML but before applying it to the page. Allows you to modify the HTML via `env.html`. There is also `env.rawHTML` for the pre-sanitization HTML. 67 | 68 | ***Credits:** Besides [Showdown](http://showdownjs.github.io/demo/), this plugin also uses the awesome [DOMPurify](https://github.com/cure53/DOMPurify) library to filter the resulting HTML.* -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | body > header .logo a { 2 | white-space: nowrap; } 3 | body > header .logo a img { 4 | display: inline-block; } 5 | body > header .logo a::after { 6 | content: "+"; 7 | display: inline-block; 8 | vertical-align: .25em; 9 | color: rgba(255, 255, 255, 0.6); } 10 | 11 | body > header + h2 { 12 | background: var(--color-blue); } 13 | 14 | .intro { 15 | font-size: 200%; } 16 | 17 | section[mv-app="plugins"] > div { 18 | display: flex; 19 | flex-flow: row wrap; 20 | justify-content: space-between; } 21 | 22 | article[property="plugin"] { 23 | width: 100%; 24 | margin: .5rem 0; } 25 | @media (min-width: 950px) { 26 | article[property="plugin"] { 27 | width: 46%; } } 28 | 29 | .plugin { 30 | display: flex; 31 | flex-flow: column; 32 | vertical-align: top; 33 | padding: .75em 1rem; 34 | border: 1px solid rgba(0, 0, 0, 0.15); 35 | border-radius: .2em; } 36 | .plugin > div { 37 | flex: 1; } 38 | .plugin h1 { 39 | margin: 0; 40 | font-size: 160%; 41 | font-weight: 300; 42 | font-family: var(--font-body); } 43 | .plugin h1 a { 44 | display: flex; 45 | color: var(--color-magenta); 46 | font-weight: inherit; } 47 | .plugin h1 [property="name"] { 48 | flex: 1; } 49 | .plugin h1 [property="id"] { 50 | padding: .5em .8em; 51 | border-radius: 1em; 52 | margin: auto; 53 | background: #ffe6f2; 54 | font-size: 50%; 55 | line-height: 1; 56 | font-weight: bold; } 57 | .plugin h1 a:hover [property="id"] { 58 | background: var(--color-magenta); 59 | color: white; 60 | text-decoration: none; } 61 | .plugin footer { 62 | display: flex; 63 | padding: inherit; 64 | margin: .6em -1rem -.75em; 65 | background: rgba(0, 0, 0, 0.05); } 66 | .plugin footer > div { 67 | display: flex; 68 | align-items: center; } 69 | .plugin footer .author { 70 | flex: 1; } 71 | .plugin footer .button, .plugin footer .github-button, .plugin footer .github-buttons > a { 72 | padding: .35em .8em; 73 | border-radius: .3em; 74 | border: 1px solid rgba(0, 0, 0, 0.15); 75 | background: linear-gradient(white, transparent); 76 | font: 600 12px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; 77 | color: inherit; } 78 | .plugin footer .button:hover, .plugin footer .github-button:hover, .plugin footer .github-buttons > a:hover { 79 | background: var(--color-magenta); 80 | color: white; 81 | text-decoration: none; } 82 | .plugin footer .button, 83 | .plugin footer .github-button, 84 | .plugin footer .github-buttons > span, 85 | .plugin footer iframe { 86 | margin: 0 .1em; } 87 | .plugin footer a { 88 | font-weight: bold; 89 | color: var(--color-magenta); } 90 | .plugin[mv-app] { 91 | font-size: 140%; } 92 | .plugin[mv-app][mv-progress]::after { 93 | font-size: calc(100% / 1.4); } 94 | 95 | a.author img { 96 | display: inline-block; 97 | vertical-align: text-top; 98 | max-height: 1.2em; 99 | border-radius: .1em; } 100 | 101 | a[property="tag"] { 102 | display: inline-block; 103 | padding: .1em .4em; 104 | margin-right: .3em; 105 | border-radius: .3em; 106 | background: var(--color-orange); 107 | color: white; 108 | font-weight: bold; 109 | text-transform: uppercase; 110 | font-size: 75%; } 111 | a[property="tag"]:hover, a[property="tag"]:focus { 112 | text-decoration: none; 113 | background: var(--color-blue); } 114 | 115 | [mv-app="pluginreadme"] h1 { 116 | color: var(--color-orange); 117 | font-size: 200%; } 118 | 119 | [mv-app="pluginreadme"] h2 { 120 | color: var(--color-magenta); 121 | font-size: 150%; } 122 | 123 | iframe { 124 | border: none; 125 | width: 100%; } 126 | 127 | /*# sourceMappingURL=style.css.map */ 128 | -------------------------------------------------------------------------------- /github-pr/README.md: -------------------------------------------------------------------------------- 1 | # GitHub Pull Requests 2 | 3 | This plugin allows you to send pull requests when using Github as a backend. 4 | 5 | ## Basics 6 | 7 | ![Show me my data](/github-pr/fork-repo.png) 8 | 9 | If the logged-in user **does not have commit permissions** (i.e., is neither the creator of the repo nor added as a collaborator), the app data will be stored in their own profile, and they will be prompted to send an “edit suggestion” to the original owner of the data. 10 | 11 | ![Send edit suggestions](/github-pr/send-edit-suggestions.png) 12 | 13 | Editing suggestions are sent as [pull requests](https://help.github.com/articles/about-pull-requests/), with a link to view the new data in the Mavo app (by using the [`storage` URL parameter](https://mavo.io/docs/storage#url-params)). 14 | 15 | ![Revoke edit suggestions](/github-pr/revoke-edit-suggestions.png) 16 | 17 | Currently, reviewing the changes between the new and old data (i.e., the *diff*) requires the ability to read JSON (unless you’re using a different format for your data). Future Mavo versions and also new versions of this plugin may introduce a graphical way for this as well. 18 | 19 | ![Review changes](/github-pr/diff-read-json.png) 20 | 21 | ## Caveats 22 | 23 | Image uploads can introduce some bizarre issues when using Github “edit suggestions” (pull requests). Since image URLs are stored in the data and don't change once you merge the pull request, they will always point to the fork that created the edit suggestion. Even worse, if the repo uses Github Pages or you use a custom URL via `mv-upload-url`, the URLs will be broken until the pull request is merged, and the user won’t know what they did wrong. Given that Mavo is client-side and cannot execute code upon merging the pull request, we are still unsure how to address this. You can add your thoughts to [issue #538](https://github.com/mavoweb/mavo/issues/538). 24 | 25 | ## Customizing Text & Localization 26 | 27 | The plugin provides a set of phrases you can use, change, and localize. Here is the list of `id`s of these phrases and their default values: 28 | 29 | | id | Default Value | 30 | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | 31 | | `gh-edit-suggestion-saved-in-profile` | Your edits are saved to your own profile, because you are not allowed to edit this page. | 32 | | `gh-edit-suggestion-instructions` | Write a short description of your edits below to suggest them to the page admins: | 33 | | `gh-edit-suggestion-notreviewed` | You have selected to suggest your edits to the page admins. Your suggestions have not been reviewed yet. | 34 | | `gh-edit-suggestion-send` | Send edit suggestion | 35 | | `gh-edit-suggestion-revoke` | Revoke edit suggestion | 36 | | `gh-edit-suggestion-reason-placeholder` | I added / corrected / deleted ... | 37 | | `gh-edit-suggestion-cancelled` | Edit suggestion cancelled successfully! | 38 | | `gh-edit-suggestion-title` | Suggested edits to data | 39 | | `gh-edit-suggestion-body` | Hello there! I used Mavo to suggest the following edits:{description} Preview my changes here: {previewURL} | 40 | | `gh-edit-suggestion-sent` | Edit suggestion sent successfully! | 41 | -------------------------------------------------------------------------------- /markdown/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Markdown tests 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Markdown tests

13 | 14 |
15 |

Basic

16 | 17 | 18 | 19 | 28 | 35 | 36 | 37 | 40 | 43 | 44 | 45 | 53 | 62 | 63 |
20 |
21 | ## Heading 22 | Hello, I am **Lea Verou**. [Mavo](http://mavo.io) is awesome. 23 | 24 | ```js 25 | Mavo.init(); 26 | ```
27 |
29 |
30 |

Heading

31 |

Hello, I am Lea Verou. Mavo is awesome.

32 |
Mavo.init();
33 |
34 |
38 | [markdown] 39 | 41 | 42 |
46 |
47 | # Heading 48 | 49 | - [x] This task is done 50 | - [ ] This is still pending 51 |
52 |
54 |
55 |

Heading

56 |
    57 |
  • This task is done
  • 58 |
  • This is still pending
  • 59 |
60 |
61 |
64 |
65 | 66 |

67 |

68 | 69 |
70 |

Rendered

71 | 72 | 73 | 74 | 79 | 82 | 83 | 84 | 87 | 90 | 91 |
75 |
{
 76 | 	"markdown": "Foo **bar** [baz](http://mavo.io) <img src=123 onerror=\"console.log('XSS')\" />"
 77 | }
78 |
80 | 81 |
85 |
86 |
88 |

Foo bar baz

89 |
92 |
93 | 94 |
95 |

Rendered asynchronously

96 | 97 | 98 | 99 | 102 | 108 | 109 | 110 | 111 | 115 | 116 |
100 |
101 |
103 |
104 |

Heading

105 |

Paragraph bold italic

106 |
107 |
[content]# Heading 112 | 113 | Paragraph **bold** *italic* 114 |
117 |
118 | 119 |
120 |

Default value with nonexistent remote storage

121 | 122 | 123 | 132 | 139 | 140 |
124 |
125 | ## Heading 126 | Hello, I am **Lea Verou**. [Mavo](http://mavo.io) is awesome. 127 | 128 | ```js 129 | Mavo.init(); 130 | ```
131 |
133 |
134 |

Heading

135 |

Hello, I am Lea Verou. Mavo is awesome.

136 |
Mavo.init();
137 |
138 |
141 |
142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /importhtml/README.md: -------------------------------------------------------------------------------- 1 | # Import HTML 2 | 3 | This plugin allows you to set Mavo attributes (e.g. `property`, `mv-multiple` etc) via CSS properties. 4 | Because CSS selectors are great at handling HTML elements en masse, this means you can use it to import data from your existing HTML, scrape data from HTML, or anything else you can think of! 5 | 6 | ## Names and syntax of CSS properties 7 | 8 | Every CSS property this plugin defines starts with `--mv-`, whether the corresponding attribute starts with `mv-` or not. 9 | This means that e.g. `property` becomes `--mv-property` and `mv-multiple` becomes `--mv-multiple`. 10 | 11 | Their values are the same as the corresponding attributes (no quotes), with one exception: 12 | Attributes without values (like `mv-multiple`) would need a value of `true`. 13 | 14 | ## Example 15 | 16 | Let's suppose you have some HTML like the following, that you’d like to turn into a Mavo app: 17 | 18 | ```html 19 |
20 |
21 |

Heading1

22 |

Content1

23 |
24 |
25 |

Heading2

26 |

Content2

27 |
28 |
29 |

Heading3

30 |

Content3

31 |
32 |
33 |

Heading4

34 |

Content4

35 |
36 |
37 |

Heading5

38 |

Content5

39 |
40 | ... 41 |
42 | ``` 43 | 44 | You *could* add `property` and `mv-multiple` attributes to every `
`, `

` or `

` element there and it would work, but it would be a huge hassle. Thankfully, you don’t have to do that! With this plugin, you can just add a few simple CSS rules: 45 | 46 | ```html 47 | 66 | ``` 67 | 68 | Reload the page, save, done! Once you save, your data is imported wherever your `mv-storage` attribute is pointing. 69 | You could now even delete the HTML (except the first item) or leave it there, up to you! 70 | 71 | You can also mix and match attributes and CSS properties! The example above could also be written as follows. 72 | 73 |

74 | 75 | ```html 76 | 90 |
91 |
92 |

Heading1

93 |

Content1

94 |
95 |
96 |

Heading2

97 |

Content2

98 |
99 |
100 |

Heading3

101 |

Content3

102 |
103 |
104 |

Heading4

105 |

Content4

106 |
107 |
108 |

Heading5

109 |

Content5

110 |
111 | ... 112 |
113 | ``` 114 | 115 | ## Caveats & Limitations 116 | 117 | - The CSS that contains these properties **must** be either a `