├── .nojekyll ├── LEAD.md ├── livemark ├── plugins │ ├── __init__.py │ ├── blog │ │ ├── __init__.py │ │ ├── markup.html │ │ ├── style.css │ │ ├── index.md │ │ └── plugin.py │ ├── file │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ ├── html │ │ ├── __init__.py │ │ ├── plugin.py │ │ └── renderer.py │ ├── map │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ ├── news │ │ ├── __init__.py │ │ ├── style.css │ │ ├── markup.html │ │ └── plugin.py │ ├── site │ │ ├── __init__.py │ │ ├── markup.html │ │ └── script.js │ ├── tabs │ │ ├── __init__.py │ │ ├── style.css │ │ ├── markup.html │ │ └── plugin.py │ ├── task │ │ ├── __init__.py │ │ ├── style.css │ │ └── plugin.py │ ├── about │ │ ├── __init__.py │ │ ├── style.css │ │ ├── markup.html │ │ └── plugin.py │ ├── audio │ │ ├── __init__.py │ │ ├── style.css │ │ ├── base.html │ │ ├── soundcloud.html │ │ └── plugin.py │ ├── brand │ │ ├── __init__.py │ │ ├── markup.html │ │ ├── style.css │ │ └── plugin.py │ ├── cards │ │ ├── __init__.py │ │ ├── style.css │ │ ├── markup.html │ │ ├── script.js │ │ └── plugin.py │ ├── chart │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ ├── github │ │ ├── __init__.py │ │ └── plugin.py │ ├── image │ │ ├── __init__.py │ │ ├── style.css │ │ ├── base.html │ │ └── plugin.py │ ├── links │ │ ├── __init__.py │ │ ├── markup.html │ │ ├── style.css │ │ └── plugin.py │ ├── logic │ │ ├── __init__.py │ │ └── plugin.py │ ├── markup │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ ├── mobile │ │ ├── __init__.py │ │ ├── markup.html │ │ ├── plugin.py │ │ ├── script.js │ │ └── style.css │ ├── notes │ │ ├── __init__.py │ │ ├── markup.html │ │ ├── style.css │ │ └── plugin.py │ ├── pages │ │ ├── __init__.py │ │ ├── script.js │ │ ├── markup.html │ │ ├── style.css │ │ └── plugin.py │ ├── rating │ │ ├── __init__.py │ │ ├── style.css │ │ ├── markup.html │ │ └── plugin.py │ ├── remark │ │ ├── __init__.py │ │ ├── style.css │ │ ├── markup.html │ │ └── plugin.py │ ├── report │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ ├── schema │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ ├── script │ │ ├── __init__.py │ │ └── plugin.py │ ├── search │ │ ├── __init__.py │ │ ├── markup.html │ │ ├── plugin.py │ │ ├── style.css │ │ └── script.js │ ├── signs │ │ ├── __init__.py │ │ ├── style.css │ │ ├── markup.html │ │ └── plugin.py │ ├── source │ │ ├── __init__.py │ │ ├── plugin.py │ │ ├── style.css │ │ └── script.js │ ├── table │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ ├── topics │ │ ├── __init__.py │ │ ├── markup.html │ │ ├── plugin.py │ │ ├── style.css │ │ └── script.js │ ├── video │ │ ├── __init__.py │ │ ├── style.css │ │ ├── base.html │ │ ├── youtube.html │ │ └── plugin.py │ ├── cleanup │ │ ├── __init__.py │ │ └── plugin.py │ ├── columns │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ ├── comments │ │ ├── __init__.py │ │ ├── markup.html │ │ ├── style.css │ │ ├── script.js │ │ └── plugin.py │ ├── counter │ │ ├── __init__.py │ │ ├── plausible.html │ │ ├── google.html │ │ ├── markup.html │ │ └── plugin.py │ ├── display │ │ ├── __init__.py │ │ ├── markup.html │ │ ├── plugin.py │ │ ├── script.js │ │ └── style.css │ ├── infinity │ │ ├── __init__.py │ │ ├── style.css │ │ ├── plugin.py │ │ └── script.js │ ├── markdown │ │ ├── __init__.py │ │ ├── plugin.py │ │ └── renderer.py │ ├── notebook │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ ├── package │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ ├── pipeline │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ ├── prepare │ │ ├── __init__.py │ │ └── plugin.py │ ├── redirect │ │ ├── __init__.py │ │ ├── missing.md │ │ ├── redirect.html │ │ └── plugin.py │ ├── reference │ │ ├── __init__.py │ │ ├── style.css │ │ └── plugin.py │ ├── resource │ │ ├── __init__.py │ │ ├── markup.html │ │ └── plugin.py │ └── pagination │ │ ├── __init__.py │ │ ├── style.css │ │ ├── plugin.py │ │ └── script.js ├── assets │ ├── VERSION │ └── documents │ │ └── template.md ├── errors.py ├── __main__.py ├── program │ ├── __init__.py │ ├── main.py │ ├── common.py │ ├── start.py │ ├── build.py │ ├── merge.py │ └── run.py ├── __init__.py ├── settings.py ├── server.py ├── config.py ├── system.py ├── snippet.py └── plugin.py ├── CNAME ├── setup.cfg ├── pyproject.toml ├── data ├── invalid.md ├── diff.md ├── preface.md ├── plugins.csv ├── livemarks.csv ├── cars.schema.json ├── brent-years.csv ├── cars.csv └── cars.report.json ├── assets ├── about.png ├── brand.png ├── flow.png ├── links.png ├── logo.png ├── news.png ├── notes.png ├── pages.png ├── signs.png ├── content.png ├── deploy.png ├── display.png ├── example.png ├── github.png ├── mobile.png ├── package.png ├── rating.png ├── report.png ├── schema.png ├── scroll.png ├── search.png ├── sidebar.png ├── source.png ├── topics.png ├── youtube.png ├── education.png ├── soundcloud.png ├── data-package.png ├── table-schema.png ├── covid-tracker.png └── data-resource.png ├── .github ├── codecov.yaml ├── issue_template.md ├── pull_request_template.md ├── stale.yml └── workflows │ └── general.yaml ├── pages ├── forum.md ├── getting-started │ ├── troubleshooting.md │ ├── configuration.md │ ├── installation.md │ ├── starting-project.md │ ├── building-website.md │ └── using-markdown.md ├── plugin-system │ ├── reference.md │ ├── architecture.md │ ├── adding-plugin.md │ └── writing-plugin.md ├── release.md ├── universe │ ├── plugins.md │ └── projects.md ├── feature-reference │ ├── blogging.md │ ├── general.md │ ├── others.md │ └── appearance.md └── guides-and-tutorials │ ├── software-education.md │ └── python-development.md ├── MANIFEST.in ├── pylama.ini ├── tests ├── test_server.py ├── test_project.py ├── test_system.py ├── program │ ├── test_merge.py │ ├── test_build.py │ └── test_main.py ├── test_markup.py └── test_snippet.py ├── README.md ├── 404.md ├── pytest.ini ├── style.css ├── blog ├── index.md └── 2021-09-01-meet-livemark.md ├── CONTRIBUTING.md ├── Makefile ├── LICENSE.md ├── .gitignore ├── livemark.yaml └── setup.py /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LEAD.md: -------------------------------------------------------------------------------- 1 | roll 2 | -------------------------------------------------------------------------------- /livemark/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | livemark.frictionlessdata.io -------------------------------------------------------------------------------- /livemark/assets/VERSION: -------------------------------------------------------------------------------- 1 | 0.110.8 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 90 3 | -------------------------------------------------------------------------------- /livemark/plugins/blog/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import BlogPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/file/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import FilePlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/html/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import HtmlPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/map/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import MapPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/news/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import NewsPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/site/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import SitePlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/tabs/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import TabsPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/task/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import TaskPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/about/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import AboutPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/audio/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import AudioPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/brand/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import BrandPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/cards/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import CardsPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/chart/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import ChartPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/github/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import GithubPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/image/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import ImagePlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/links/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import LinksPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/logic/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import LogicPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/markup/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import MarkupPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/mobile/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import MobilePlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/notes/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import NotesPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/pages/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import PagesPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/rating/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import RatingPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/remark/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import RemarkPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/report/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import ReportPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/schema/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import SchemaPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/script/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import ScriptPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/search/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import SearchPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/signs/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import SignsPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/source/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import SourcePlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/table/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import TablePlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/topics/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import TopicsPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/video/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import VideoPlugin 2 | -------------------------------------------------------------------------------- /data/invalid.md: -------------------------------------------------------------------------------- 1 | --- 2 | brand: 3 | text: 1 4 | --- 5 | 6 | # Preface 7 | -------------------------------------------------------------------------------- /livemark/plugins/cleanup/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import CleanupPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/columns/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import ColumnsPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/comments/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import CommentsPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/counter/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import CounterPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/display/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import DisplayPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/infinity/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import InfinityPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/markdown/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import MarkdownPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/notebook/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import NotebookPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/package/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import PackagePlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/pipeline/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import PipelinePlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/prepare/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import PreparePlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/redirect/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import RedirectPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/reference/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import ReferencePlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/resource/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import ResourcePlugin 2 | -------------------------------------------------------------------------------- /data/diff.md: -------------------------------------------------------------------------------- 1 | # Diff 2 | 3 | ```python script 4 | print('Hello World') 5 | ``` 6 | -------------------------------------------------------------------------------- /data/preface.md: -------------------------------------------------------------------------------- 1 | --- 2 | brand: 3 | text: Livemark 4 | --- 5 | 6 | # Preface 7 | -------------------------------------------------------------------------------- /livemark/plugins/about/style.css: -------------------------------------------------------------------------------- 1 | #livemark-about { 2 | color: #888; 3 | } 4 | -------------------------------------------------------------------------------- /livemark/plugins/pagination/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import PaginationPlugin 2 | -------------------------------------------------------------------------------- /livemark/plugins/audio/style.css: -------------------------------------------------------------------------------- 1 | .livemark-audio { 2 | padding-bottom: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /livemark/plugins/image/style.css: -------------------------------------------------------------------------------- 1 | .livemark-image { 2 | padding-bottom: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /livemark/plugins/infinity/style.css: -------------------------------------------------------------------------------- 1 | .livemark-infinity { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /livemark/plugins/remark/style.css: -------------------------------------------------------------------------------- 1 | .livemark-remark { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /livemark/plugins/video/style.css: -------------------------------------------------------------------------------- 1 | .livemark-video { 2 | padding-bottom: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /assets/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/about.png -------------------------------------------------------------------------------- /assets/brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/brand.png -------------------------------------------------------------------------------- /assets/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/flow.png -------------------------------------------------------------------------------- /assets/links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/links.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/news.png -------------------------------------------------------------------------------- /assets/notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/notes.png -------------------------------------------------------------------------------- /assets/pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/pages.png -------------------------------------------------------------------------------- /assets/signs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/signs.png -------------------------------------------------------------------------------- /livemark/errors.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | """Livemark error""" 3 | 4 | pass 5 | -------------------------------------------------------------------------------- /livemark/plugins/pagination/style.css: -------------------------------------------------------------------------------- 1 | .livemark-pagination { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /assets/content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/content.png -------------------------------------------------------------------------------- /assets/deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/deploy.png -------------------------------------------------------------------------------- /assets/display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/display.png -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/example.png -------------------------------------------------------------------------------- /assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/github.png -------------------------------------------------------------------------------- /assets/mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/mobile.png -------------------------------------------------------------------------------- /assets/package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/package.png -------------------------------------------------------------------------------- /assets/rating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/rating.png -------------------------------------------------------------------------------- /assets/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/report.png -------------------------------------------------------------------------------- /assets/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/schema.png -------------------------------------------------------------------------------- /assets/scroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/scroll.png -------------------------------------------------------------------------------- /assets/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/search.png -------------------------------------------------------------------------------- /assets/sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/sidebar.png -------------------------------------------------------------------------------- /assets/source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/source.png -------------------------------------------------------------------------------- /assets/topics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/topics.png -------------------------------------------------------------------------------- /assets/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/youtube.png -------------------------------------------------------------------------------- /livemark/plugins/cards/style.css: -------------------------------------------------------------------------------- 1 | #livemark-cards .modal-lg { 2 | max-width: 1000px; 3 | } 4 | -------------------------------------------------------------------------------- /assets/education.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/education.png -------------------------------------------------------------------------------- /assets/soundcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/soundcloud.png -------------------------------------------------------------------------------- /assets/data-package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/data-package.png -------------------------------------------------------------------------------- /assets/table-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/table-schema.png -------------------------------------------------------------------------------- /livemark/plugins/file/markup.html: -------------------------------------------------------------------------------- 1 |
{{ text }}
2 | 
3 | -------------------------------------------------------------------------------- /assets/covid-tracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/covid-tracker.png -------------------------------------------------------------------------------- /assets/data-resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frictionlessdata/livemark/HEAD/assets/data-resource.png -------------------------------------------------------------------------------- /livemark/plugins/comments/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /livemark/plugins/topics/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | -------------------------------------------------------------------------------- /.github/codecov.yaml: -------------------------------------------------------------------------------- 1 | comment: no 2 | coverage: 3 | range: 80..90 4 | status: 5 | project: false 6 | patch: false 7 | -------------------------------------------------------------------------------- /livemark/plugins/about/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ plugin.text }} 4 |
5 |
6 | -------------------------------------------------------------------------------- /data/plugins.csv: -------------------------------------------------------------------------------- 1 | user,repo,branch,stars,description 2 | frictionlessdata,livemark-ckan,main,1,It's a CKAN plugin for Livemark 3 | -------------------------------------------------------------------------------- /livemark/__main__.py: -------------------------------------------------------------------------------- 1 | from .program import program 2 | 3 | 4 | if __name__ == "__main__": 5 | program(prog_name="livemark") 6 | -------------------------------------------------------------------------------- /livemark/plugins/notebook/markup.html: -------------------------------------------------------------------------------- 1 |
2 | Notebooks support is under construction 3 |
4 | -------------------------------------------------------------------------------- /pages/forum.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: 3 | code: livemark 4 | link: https://livemark.frictionlessdata.io 5 | --- 6 | 7 | # Forum 8 | -------------------------------------------------------------------------------- /livemark/plugins/counter/plausible.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /livemark/plugins/task/style.css: -------------------------------------------------------------------------------- 1 | .livemark-task pre:first-child { 2 | margin-bottom: 0 !important; 3 | border-bottom: dashed 1px #ccc; 4 | } 5 | -------------------------------------------------------------------------------- /livemark/plugins/comments/style.css: -------------------------------------------------------------------------------- 1 | #livemark-comments { 2 | border-top: solid 1px #eaecef; 3 | margin-top: 16px; 4 | padding-top: 16px; 5 | } 6 | -------------------------------------------------------------------------------- /livemark/plugins/tabs/style.css: -------------------------------------------------------------------------------- 1 | .nav-tabs { 2 | padding-left: 0 !important; 3 | } 4 | 5 | .nav-item { 6 | margin-top: 0 !important; 7 | } 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include livemark * 2 | include LICENSE.md 3 | include Makefile 4 | include pylama.ini 5 | include pytest.ini 6 | include README.md 7 | -------------------------------------------------------------------------------- /livemark/plugins/remark/markup.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Please replace this line with full information about your idea or problem. If it's a bug share as much as possible to reproduce it 4 | -------------------------------------------------------------------------------- /livemark/plugins/rating/style.css: -------------------------------------------------------------------------------- 1 | #livemark-rating { 2 | height: 46px; 3 | } 4 | 5 | #livemark-rating iframe { 6 | margin-top: -5px; 7 | border: none; 8 | opacity: 0.5; 9 | } 10 | -------------------------------------------------------------------------------- /livemark/plugins/resource/markup.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ resource.title }}

3 |

{{ resource.description_text or 'No description provided' }}

4 |
5 | -------------------------------------------------------------------------------- /livemark/plugins/blog/markup.html: -------------------------------------------------------------------------------- 1 |

2 | By {{ plugin.author }} on {{ plugin.date }} » 3 | Blog Index 4 |

5 | -------------------------------------------------------------------------------- /livemark/plugins/blog/style.css: -------------------------------------------------------------------------------- 1 | .livemark-blog-item { 2 | margin-bottom: 16px; 3 | text-align: justify; 4 | } 5 | 6 | .livemark-blog-item h2 a:not(.heading) { 7 | color: #222 !important; 8 | } 9 | -------------------------------------------------------------------------------- /livemark/plugins/image/base.html: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 | -------------------------------------------------------------------------------- /livemark/plugins/search/markup.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /livemark/plugins/task/plugin.py: -------------------------------------------------------------------------------- 1 | from ... import Plugin 2 | 3 | 4 | class TaskPlugin(Plugin): 5 | identity = "task" 6 | 7 | def process_markup(self, markup): 8 | markup.add_style("style.css") 9 | -------------------------------------------------------------------------------- /pylama.ini: -------------------------------------------------------------------------------- 1 | [pylama] 2 | linters = pyflakes,mccabe,pep8 3 | ignore = E128,E203,E301,E501,E731,C901 4 | 5 | [pylama:mccabe] 6 | complexity = 48 7 | 8 | [pylama:*/__init__.py] 9 | ignore = W0611,W0401 10 | -------------------------------------------------------------------------------- /livemark/program/__init__.py: -------------------------------------------------------------------------------- 1 | from .build import program_build 2 | from .main import program, program_main 3 | from .merge import program_merge 4 | from .run import program_run 5 | from .start import program_start 6 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | from livemark import Server, Project 2 | 3 | 4 | def test_server(): 5 | project = Project(config="livemark.yaml") 6 | server = Server(project) 7 | assert server.project is project 8 | -------------------------------------------------------------------------------- /livemark/plugins/brand/markup.html: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | -------------------------------------------------------------------------------- /livemark/plugins/mobile/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |
7 |
8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Livemark 2 | 3 | Data presentation framework for Python that generates static sites from extended Markdown with interactive charts, tables, scripts, and other features: 4 | 5 | - https://livemark.frictionlessdata.io/ 6 | 7 | -------------------------------------------------------------------------------- /404.md: -------------------------------------------------------------------------------- 1 | --- 2 | site: 3 | description: This page is not found 4 | signs: false 5 | --- 6 | 7 | # Not Found 8 | 9 | ```markdown remark type=danger 10 | This page is not found 11 | ``` 12 | 13 | Return to the home page. 14 | -------------------------------------------------------------------------------- /livemark/plugins/audio/base.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 | -------------------------------------------------------------------------------- /livemark/plugins/columns/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% for column in columns %} 4 |
5 | {{ column.content }} 6 |
7 | {% endfor %} 8 |
9 |
10 | -------------------------------------------------------------------------------- /livemark/plugins/pipeline/markup.html: -------------------------------------------------------------------------------- 1 |
2 | {% for task in pipeline.tasks %} 3 |

Source: {{ task.source }}

4 |

Type: {{ task.type }}

5 |

Steps: {{ task.steps }}

6 | {% endfor %} 7 |
8 | -------------------------------------------------------------------------------- /livemark/plugins/video/base.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | markers = 4 | ci: integrational tests (select with '--ci') 5 | filterwarnings = 6 | ignore::DeprecationWarning:boto.* 7 | ignore::DeprecationWarning:moto.* 8 | ignore::DeprecationWarning:savWriter.* 9 | -------------------------------------------------------------------------------- /livemark/plugins/redirect/missing.md: -------------------------------------------------------------------------------- 1 | --- 2 | site: 3 | description: This page is not found 4 | signs: false 5 | --- 6 | 7 | # Not Found 8 | 9 | ```markdown remark type=danger 10 | This page is not found 11 | ``` 12 | 13 | Return to the home page. 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | - fixes # 2 | 3 | --- 4 | 5 | Please make sure that all the checks pass. Please add here any additional information regarding this pull request. It's highly recommended that you link this PR to an issue (please create one if it doesn't exist for this PR) 6 | -------------------------------------------------------------------------------- /livemark/plugins/infinity/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | 3 | 4 | class InfinityPlugin(Plugin): 5 | identity = "infinity" 6 | 7 | # Process 8 | 9 | def process_markup(self, markup): 10 | markup.add_style("style.css") 11 | markup.add_script("script.js") 12 | -------------------------------------------------------------------------------- /livemark/plugins/markup/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 10 | -------------------------------------------------------------------------------- /livemark/plugins/counter/google.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /livemark/plugins/news/style.css: -------------------------------------------------------------------------------- 1 | #livemark-news { 2 | display: none; 3 | position: fixed; 4 | background: yellow; 5 | border-bottom: solid 1px #dfe2e5; 6 | text-align: center; 7 | z-index: 1000000; 8 | width: 100%; 9 | top: 0; 10 | } 11 | 12 | #livemark-news-close { 13 | margin-right: 10px; 14 | } 15 | -------------------------------------------------------------------------------- /livemark/plugins/rating/markup.html: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | -------------------------------------------------------------------------------- /livemark/plugins/video/youtube.html: -------------------------------------------------------------------------------- 1 |
2 | 11 |
12 | -------------------------------------------------------------------------------- /livemark/plugins/redirect/redirect.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /livemark/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import Config 2 | from .document import Document 3 | from .markup import Markup 4 | from .plugin import Plugin 5 | from .project import Project 6 | from .program import program 7 | from .server import Server 8 | from .settings import VERSION as __version__ 9 | from .snippet import Snippet 10 | from .system import system 11 | from . import errors 12 | -------------------------------------------------------------------------------- /livemark/plugins/links/markup.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /pages/getting-started/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | Livemark is an open-source project with a publicly available [Issue Tracker](https://github.com/frictionlessdata/livemark/issues). Feel free to report any problem you have while working with Livemark as well as questions and feature requests. We will be adding the most common issues, problems, and gotchas to this page in the future. 4 | -------------------------------------------------------------------------------- /livemark/plugins/signs/style.css: -------------------------------------------------------------------------------- 1 | #livemark-signs { 2 | color: #888; 3 | border-top: 1px solid #eaecef; 4 | margin-top: 24px; 5 | padding-top: 20px; 6 | height: 50px; 7 | } 8 | 9 | #livemark-signs .next { 10 | float: right; 11 | } 12 | 13 | #livemark-signs a { 14 | font-size: 20px; 15 | color: currentColor; 16 | } 17 | 18 | #livemark-signs a:hover { 19 | color: #80b2e6; 20 | } 21 | -------------------------------------------------------------------------------- /livemark/plugins/package/markup.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ package.title }}

3 |

{{ package.description_text or 'No description provided' }}

4 |

Resources

5 | {% for resource in package.resources %} 6 |
{{ resource.title or resource.name }}
7 |

{{ resource.description_text or 'No description provided' }}

8 | {% endfor %} 9 |
10 | -------------------------------------------------------------------------------- /tests/test_project.py: -------------------------------------------------------------------------------- 1 | from livemark import Project 2 | 3 | 4 | # General 5 | 6 | 7 | def test_project(): 8 | project = Project(source="index.md") 9 | assert project.document.source == "index.md" 10 | 11 | 12 | # Read 13 | 14 | 15 | def test_project_read(): 16 | project = Project(config="livemark.yaml") 17 | project.read() 18 | assert project.config["brand"]["text"] == "Livemark" 19 | -------------------------------------------------------------------------------- /livemark/settings.py: -------------------------------------------------------------------------------- 1 | from . import helpers 2 | 3 | 4 | # General 5 | 6 | 7 | VERSION = helpers.read_asset("VERSION") 8 | TEMPLATE = helpers.path_asset("documents", "template.md") 9 | 10 | 11 | # Defaults 12 | 13 | 14 | DEFAULT_SOURCE = "index.md" 15 | DEFAULT_FORMAT = "html" 16 | DEFAULT_CONFIG = "livemark.yaml" 17 | DEFAULT_HOST = "localhost" 18 | DEFAULT_PORT = 7000 19 | DEFAULT_FILE = "index.html" 20 | -------------------------------------------------------------------------------- /livemark/plugins/report/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 12 | -------------------------------------------------------------------------------- /livemark/plugins/schema/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 12 | -------------------------------------------------------------------------------- /data/livemarks.csv: -------------------------------------------------------------------------------- 1 | user,repo,branch,stars,description 2 | frictionlessdata,covid-tracker,main,3,A livemark tracking COVID-19 disease pandemic 3 | frictionlessdata,data-packages,main,1,A livemark listing data packages hosted on Github 4 | frictionlessdata,ckan-extensions,main,1,A livemark listing CKAN extensions hosted on Github 5 | frictionlessdata,community-insights,main,1,To tell a story about the Frictionless community using Livemark 6 | -------------------------------------------------------------------------------- /livemark/plugins/source/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | 3 | 4 | # NOTE: 5 | # Consider using proper markdown parser client-side 6 | # Or embed source generation in the server-side chain 7 | 8 | 9 | class SourcePlugin(Plugin): 10 | identity = "source" 11 | 12 | # Process 13 | 14 | def process_markup(self, markup): 15 | markup.add_style("style.css") 16 | markup.add_script("script.js") 17 | -------------------------------------------------------------------------------- /livemark/plugins/notes/markup.html: -------------------------------------------------------------------------------- 1 |
2 | {% if plugin.edit_url %} 3 | Edit page in Livemark
4 | {% else %} 5 | Written in Livemark
6 | {% endif %} 7 | ({{ plugin.current.strftime(plugin.format) }}) 8 |
9 | -------------------------------------------------------------------------------- /livemark/plugins/chart/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | {% if card %} 6 | 9 | {% else %} 10 | 15 | {% endif %} 16 | -------------------------------------------------------------------------------- /livemark/plugins/reference/style.css: -------------------------------------------------------------------------------- 1 | .livemark-reference { 2 | display: block; 3 | /* font-family: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace;; */ 4 | } 5 | 6 | .livemark-reference-heading { 7 | background-color: #f6f8fa; 8 | font-family: "Roboto Mono", ui-monospace, monospace; 9 | padding: 1em 0; 10 | } 11 | -------------------------------------------------------------------------------- /livemark/plugins/audio/soundcloud.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 | -------------------------------------------------------------------------------- /livemark/plugins/pagination/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | 3 | 4 | class PaginationPlugin(Plugin): 5 | identity = "pagination" 6 | 7 | # Process 8 | 9 | def process_markup(self, markup): 10 | markup.add_style("https://unpkg.com/paginationjs@2.1.5/dist/pagination.css") 11 | markup.add_style("style.css") 12 | markup.add_script("https://unpkg.com/paginationjs@2.1.5/dist/pagination.min.js") 13 | markup.add_script("script.js") 14 | -------------------------------------------------------------------------------- /livemark/plugins/source/style.css: -------------------------------------------------------------------------------- 1 | #livemark-main h2 .livemark-source-button { 2 | float: right; 3 | display: none; 4 | } 5 | 6 | #livemark-main h2:hover .livemark-source-button { 7 | display: inline; 8 | margin-left: 8px; 9 | color: #aaa; 10 | font-weight: normal; 11 | text-decoration: none; 12 | } 13 | 14 | #livemark-main h2 .livemark-source-button:hover { 15 | text-decoration: underline; 16 | } 17 | 18 | .livemark-source-section { 19 | border: dashed 1px #ccc; 20 | } 21 | -------------------------------------------------------------------------------- /livemark/plugins/comments/script.js: -------------------------------------------------------------------------------- 1 | var disqus_config = function () { 2 | this.page.url = "{{ plugin.link }}/{{ plugin.document.path }}.html"; 3 | this.page.identifier = "{{ plugin.document.path }}"; 4 | }; 5 | (function () { 6 | // DON'T EDIT BELOW THIS LINE 7 | var d = document, 8 | s = d.createElement("script"); 9 | s.src = "https://{{ plugin.code }}.disqus.com/embed.js"; 10 | s.setAttribute("data-timestamp", +new Date()); 11 | (d.head || d.body).appendChild(s); 12 | })(); 13 | -------------------------------------------------------------------------------- /livemark/plugins/pages/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | const groups = $("#livemark-pages li.group"); 3 | for (const group of groups) { 4 | $(group) 5 | .children("a") 6 | .click((ev) => { 7 | ev.preventDefault(); 8 | $(group).toggleClass("active"); 9 | // $(group).find(".fa").toggleClass("fa-chevron-right"); 10 | // $(group).find(".fa").toggleClass("fa-chevron-down"); 11 | }); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /data/cars.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "fields": [ 3 | { 4 | "name": "brand", 5 | "type": "string" 6 | }, 7 | { 8 | "name": "model", 9 | "type": "string" 10 | }, 11 | { 12 | "name": "price", 13 | "type": "integer" 14 | }, 15 | { 16 | "name": "kmpl", 17 | "type": "number" 18 | }, 19 | { 20 | "name": "bhp", 21 | "type": "integer" 22 | }, 23 | { 24 | "name": "type", 25 | "type": "string" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /livemark/plugins/mobile/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | 3 | 4 | # This plugin is based on the following article: 5 | # https://dev.to/devggaurav/let-s-build-a-responsive-navbar-and-hamburger-menu-using-html-css-and-javascript-4gci 6 | 7 | 8 | class MobilePlugin(Plugin): 9 | identity = "mobile" 10 | 11 | # Process 12 | 13 | def process_markup(self, markup): 14 | markup.add_style("style.css") 15 | markup.add_script("script.js") 16 | markup.add_markup("markup.html") 17 | -------------------------------------------------------------------------------- /livemark/plugins/signs/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% if plugin.items.next %} 4 | 9 | {% endif %} 10 | {% if plugin.items.prev %} 11 | 16 | {% endif %} 17 |
18 |
19 | -------------------------------------------------------------------------------- /livemark/plugins/counter/markup.html: -------------------------------------------------------------------------------- 1 | {% if plugin.type == 'google'} 2 | 3 | 9 | {% elif plugin.type == 'plausible' %} 10 | 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /tests/test_system.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from livemark import Plugin, system, errors 3 | 4 | 5 | # General 6 | 7 | 8 | def test_system(): 9 | assert len(system.Plugins) > 20 10 | 11 | 12 | def test_system_register(): 13 | system.register(Plugin) 14 | assert system.Plugins[""] 15 | system.deregister(Plugin) 16 | 17 | 18 | def test_system_deregister_not_registered(): 19 | with pytest.raises(errors.Error) as excinfo: 20 | system.deregister(Plugin) 21 | assert str(excinfo.value).count("Not registered plugin") 22 | -------------------------------------------------------------------------------- /tests/program/test_merge.py: -------------------------------------------------------------------------------- 1 | from typer.testing import CliRunner 2 | from livemark import program 3 | 4 | 5 | runner = CliRunner() 6 | 7 | 8 | # General 9 | 10 | 11 | def test_program_merge(): 12 | result = runner.invoke(program, "merge index.md --print") 13 | assert result.exit_code == 0 14 | assert result.stdout.count("# Livemark") 15 | 16 | 17 | def test_program_merge_bad_source(): 18 | result = runner.invoke(program, "merge bad.md --print") 19 | assert result.exit_code == 1 20 | assert result.stdout.count("No such file") 21 | -------------------------------------------------------------------------------- /livemark/plugins/site/markup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ plugin.title }} 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /livemark/plugins/tabs/markup.html: -------------------------------------------------------------------------------- 1 | 14 |
15 | {% for tab in tabs %} 16 |
20 | {{ tab.content | safe }} 21 |
22 | {% endfor %} 23 |
24 | -------------------------------------------------------------------------------- /tests/program/test_build.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typer.testing import CliRunner 3 | from livemark import program 4 | 5 | 6 | runner = CliRunner() 7 | 8 | 9 | # General 10 | 11 | 12 | @pytest.mark.skip 13 | def test_program_build(): 14 | result = runner.invoke(program, "build index.md --print") 15 | assert result.exit_code == 0 16 | assert result.stdout.count("

Livemark

") 17 | 18 | 19 | def test_program_build_bad_source(): 20 | result = runner.invoke(program, "build bad.md --print") 21 | assert result.exit_code == 1 22 | assert result.stdout.count("No such file") 23 | -------------------------------------------------------------------------------- /livemark/plugins/display/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /livemark/plugins/brand/style.css: -------------------------------------------------------------------------------- 1 | #livemark-brand { 2 | color: #888; 3 | } 4 | 5 | #livemark-brand ul { 6 | overflow: hidden; 7 | position: relative; 8 | padding-left: 0; 9 | margin: 0; 10 | } 11 | 12 | #livemark-brand li { 13 | list-style: none; 14 | } 15 | 16 | #livemark-brand a { 17 | display: inline-block; 18 | color: currentColor; 19 | position: relative; 20 | width: 100%; 21 | line-height: 100%; 22 | padding-top: 5px; 23 | padding-bottom: 5px; 24 | } 25 | 26 | #livemark-brand a.active { 27 | font-weight: 700; 28 | } 29 | 30 | #livemark-brand a:hover { 31 | color: #80b2e6; 32 | } 33 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | .item { 2 | position: relative; 3 | border: dashed 1px #4495ef; 4 | border-radius: 10px; 5 | margin-bottom: 20px; 6 | padding: 0 20px; 7 | display: flex; 8 | align-items: center; 9 | background-color: #e8f3ff; 10 | } 11 | 12 | .item:nth-child(even) { 13 | background-color: #d1e7ff; 14 | } 15 | 16 | .item-content { 17 | width: 100%; 18 | border-right: dashed 1px #ccc; 19 | } 20 | 21 | .item-content-link { 22 | color: grey !important; 23 | } 24 | 25 | .item-stars { 26 | font-size: 24px; 27 | padding-left: 20px; 28 | } 29 | 30 | .item-stars-count { 31 | font-size: 30px; 32 | } 33 | -------------------------------------------------------------------------------- /tests/test_markup.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from livemark import Markup, errors 3 | 4 | 5 | # General 6 | 7 | 8 | def test_markup(): 9 | input = "" 10 | markup = Markup(input) 11 | assert markup.input == input 12 | assert markup.output == input 13 | assert markup.query.outer_html() == input 14 | 15 | 16 | # Bind 17 | 18 | 19 | def test_markup_get_plugin_not_bound(): 20 | input = "" 21 | markup = Markup(input) 22 | with pytest.raises(errors.Error) as excinfo: 23 | markup.add_markup("markup.html") 24 | assert str(excinfo.value).count("Markup is not bound") 25 | -------------------------------------------------------------------------------- /livemark/plugins/notebook/plugin.py: -------------------------------------------------------------------------------- 1 | # import yaml 2 | from ...plugin import Plugin 3 | 4 | 5 | class NotebookPlugin(Plugin): 6 | identity = "notebook" 7 | priority = 60 8 | 9 | # Process 10 | 11 | def process_document(self, document): 12 | self.__count = 0 13 | 14 | def process_snippet(self, snippet): 15 | if self.document.format == "html": 16 | if snippet.type == "notebook" and snippet.lang == "yaml": 17 | # spec = yaml.safe_load(str(snippet.input).strip()) 18 | self.__count += 1 19 | snippet.output = self.read_asset("markup.html") + "\n" 20 | -------------------------------------------------------------------------------- /livemark/program/main.py: -------------------------------------------------------------------------------- 1 | import typer 2 | from .. import settings 3 | from typing import Optional 4 | 5 | 6 | # Program 7 | 8 | program = typer.Typer() 9 | 10 | 11 | # Helpers 12 | 13 | 14 | def version(value: bool): 15 | if value: 16 | typer.echo(settings.VERSION) 17 | raise typer.Exit() 18 | 19 | 20 | # Command 21 | 22 | 23 | @program.callback() 24 | def program_main( 25 | version: Optional[bool] = typer.Option(None, "--version", callback=version) 26 | ): 27 | """Livemark is a Python static site generator 28 | that extends Markdown with interactive charts, tables, scripts, and other features. 29 | """ 30 | pass 31 | -------------------------------------------------------------------------------- /livemark/plugins/map/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 18 | -------------------------------------------------------------------------------- /livemark/plugins/cleanup/plugin.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from ...plugin import Plugin 3 | 4 | 5 | class CleanupPlugin(Plugin): 6 | identity = "cleanup" 7 | priority = -100 8 | validity = { 9 | "type": "object", 10 | "required": ["commands"], 11 | "properties": { 12 | "commands": {"type": "array", "items": {"type": "string"}}, 13 | }, 14 | } 15 | 16 | # Context 17 | 18 | @property 19 | def commands(self): 20 | return self.config.get("commands", []) 21 | 22 | # Process 23 | 24 | def process_document(self, document): 25 | for code in self.commands: 26 | subprocess.run(code, shell=True) 27 | -------------------------------------------------------------------------------- /livemark/plugins/prepare/plugin.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from ...plugin import Plugin 3 | 4 | 5 | class PreparePlugin(Plugin): 6 | identity = "prepare" 7 | priority = 100 8 | validity = { 9 | "type": "object", 10 | "required": ["commands"], 11 | "properties": { 12 | "commands": {"type": "array", "items": {"type": "string"}}, 13 | }, 14 | } 15 | 16 | # Context 17 | 18 | @property 19 | def commands(self): 20 | return self.config.get("commands", []) 21 | 22 | # Process 23 | 24 | def process_document(self, document): 25 | for code in self.commands: 26 | subprocess.run(code, shell=True) 27 | -------------------------------------------------------------------------------- /livemark/plugins/news/markup.html: -------------------------------------------------------------------------------- 1 |
2 | {{ plugin.text }} 3 | 6 |
7 | 18 | -------------------------------------------------------------------------------- /livemark/plugins/file/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | from ... import helpers 3 | 4 | 5 | # NOTE: 6 | # We'd like to be able to process it even for a markdown target (as scripts) 7 | # To achieve it we need to update the protocol that HtmlRenderer uses for snippets 8 | 9 | 10 | class FilePlugin(Plugin): 11 | identity = "file" 12 | 13 | # Process 14 | 15 | def process_snippet(self, snippet): 16 | if self.document.format == "html": 17 | if snippet.type == "file": 18 | lang = snippet.lang 19 | text = helpers.read_file(snippet.input.strip()).strip() 20 | snippet.output = self.read_asset("markup.html", lang=lang, text=text) 21 | -------------------------------------------------------------------------------- /livemark/plugins/pagination/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | const container = $(".livemark-pagination"); 3 | if (container.length) { 4 | const elements = container 5 | .children() 6 | .map((index, element) => element.outerHTML) 7 | .get(); 8 | container.html(` 9 |
10 |
11 | `); 12 | container.find(".livemark-pagination-navs").pagination({ 13 | dataSource: elements, 14 | callback: (html) => { 15 | container.find(".livemark-pagination-data").html(html); 16 | }, 17 | }); 18 | container.show(); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /livemark/plugins/html/plugin.py: -------------------------------------------------------------------------------- 1 | import marko 2 | from marko.ext.gfm import GFM 3 | from .renderer import HtmlExtension 4 | from ...plugin import Plugin 5 | 6 | 7 | class HtmlPlugin(Plugin): 8 | identity = "html" 9 | priority = 20 10 | 11 | # Process 12 | 13 | def process_document(self, document): 14 | if document.format == "html": 15 | markdown = marko.Markdown() 16 | markdown.use(GFM) 17 | markdown.use(HtmlExtension) 18 | output = markdown.parse(document.content) 19 | markdown.renderer.document = document 20 | output = markdown.render(output) 21 | output = output.strip() 22 | document.output = output 23 | -------------------------------------------------------------------------------- /livemark/plugins/markdown/plugin.py: -------------------------------------------------------------------------------- 1 | import marko 2 | from .renderer import MarkdownRenderer 3 | from ...plugin import Plugin 4 | 5 | 6 | class MarkdownPlugin(Plugin): 7 | identity = "markdown" 8 | priority = 20 9 | 10 | # Process 11 | 12 | def process_document(self, document): 13 | if document.format == "md": 14 | markdown = marko.Markdown(renderer=MarkdownRenderer) 15 | output = markdown.parse(document.content) 16 | markdown.renderer.document = document 17 | output = markdown.render(output) 18 | if document.preface: 19 | output = document.preface.join(["---"] * 2) + "\n\n" + output 20 | document.output = output 21 | -------------------------------------------------------------------------------- /livemark/plugins/mobile/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | const left = document.getElementById("livemark-left"); 3 | const mobile = document.getElementById("livemark-mobile"); 4 | mobile.addEventListener("click", () => { 5 | left.classList.toggle("active"); 6 | mobile.classList.toggle("active"); 7 | }); 8 | // NOTE: We can replace the selector by 'a:not[href=""]' after #57 9 | left.querySelectorAll("li:not(.group) a").forEach((link) => { 10 | link.addEventListener("click", () => { 11 | if (left.classList.contains("active")) { 12 | left.classList.remove("active"); 13 | mobile.classList.remove("active"); 14 | } 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /data/brent-years.csv: -------------------------------------------------------------------------------- 1 | Date,Price 2 | 1987-06-30,18.53 3 | 1988-06-30,14.91 4 | 1989-06-30,18.23 5 | 1990-06-30,23.76 6 | 1991-06-30,20.04 7 | 1992-06-30,19.32 8 | 1993-06-30,17.01 9 | 1994-06-30,15.86 10 | 1995-06-30,17.02 11 | 1996-06-30,20.64 12 | 1997-06-30,19.11 13 | 1998-06-30,12.76 14 | 1999-06-30,17.9 15 | 2000-06-30,28.66 16 | 2001-06-30,24.46 17 | 2002-06-30,24.99 18 | 2003-06-30,28.85 19 | 2004-06-30,38.26 20 | 2005-06-30,54.57 21 | 2006-06-30,65.16 22 | 2007-06-30,72.44 23 | 2008-06-30,96.94 24 | 2009-06-30,61.74 25 | 2010-06-30,79.61 26 | 2011-06-30,111.26 27 | 2012-06-30,111.63 28 | 2013-06-30,108.56 29 | 2014-06-30,98.97 30 | 2015-06-30,52.32 31 | 2016-06-30,43.64 32 | 2017-06-30,54.13 33 | 2018-06-30,71.34 34 | 2019-06-30,64.3 35 | -------------------------------------------------------------------------------- /livemark/plugins/infinity/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | const container = $(".livemark-infinity"); 3 | if (container.length) { 4 | const elements = container 5 | .children() 6 | .map((index, element) => element.outerHTML) 7 | .get(); 8 | container.html(elements.splice(0, 100)); 9 | container.show(); 10 | window.addEventListener("scroll", () => { 11 | const element = container.get(0); 12 | const position = window.scrollY + window.innerHeight + 100; 13 | const threshold = element.offsetTop + element.scrollHeight; 14 | if (position > threshold) { 15 | container.append(elements.splice(0, 100)); 16 | } 17 | }); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /livemark/plugins/cards/markup.html: -------------------------------------------------------------------------------- 1 |
2 | 19 |
20 | -------------------------------------------------------------------------------- /blog/index.md: -------------------------------------------------------------------------------- 1 | # Blog 2 | 3 | ```html markup 4 | {% for item in document.get_plugin('blog').items %} 5 |
6 |

{{ item.document.name }}

7 |
8 |
9 |

10 | 11 | By {{ item.document.get_plugin('blog').author }} 12 | on {{ item.document.get_plugin('blog').date }} 13 | 14 |

15 | {{ item.document.get_plugin('blog').description }} 16 | Read more » 17 |
18 |
19 | 20 |
21 |
22 |
23 | {% endfor %} 24 | ``` 25 | -------------------------------------------------------------------------------- /livemark/plugins/blog/index.md: -------------------------------------------------------------------------------- 1 | # Blog 2 | 3 | ```html markup 4 | {% for item in document.get_plugin('blog').items %} 5 |
6 |

{{ item.document.name }}

7 |
8 |
9 |

10 | 11 | By {{ item.document.get_plugin('blog').author }} 12 | on {{ item.document.get_plugin('blog').date }} 13 | 14 |

15 | {{ item.document.get_plugin('blog').description }} 16 | Read more » 17 |
18 |
19 | 20 |
21 |
22 |
23 | {% endfor %} 24 | ``` 25 | -------------------------------------------------------------------------------- /livemark/plugins/notes/style.css: -------------------------------------------------------------------------------- 1 | #livemark-notes { 2 | color: #aaa; 3 | text-align: right; 4 | font-size: 14px; 5 | float: right; 6 | visibility: hidden; 7 | } 8 | 9 | @media (min-width: 768px) { 10 | #livemark-notes { 11 | visibility: visible; 12 | } 13 | } 14 | 15 | #livemark-notes a { 16 | color: inherit; 17 | } 18 | 19 | #livemark-notes a:hover { 20 | color: #80b2e6; 21 | } 22 | 23 | #livemark-notes a[target="_blank"]:after { 24 | content: "\f35d"; 25 | font-family: "Font Awesome 5 Free"; 26 | font-weight: 900; 27 | vertical-align: text-top; 28 | text-decoration: none; 29 | display: inline-block; 30 | color: #ccc; 31 | font-size: 10px; 32 | margin-left: -1px; 33 | } 34 | 35 | #livemark-notes a[target="_blank"]:hover:after { 36 | color: #80b2e6; 37 | } 38 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 90 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 30 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - feature 10 | - enhancement 11 | - bug 12 | 13 | # Label to use when marking an issue as stale 14 | staleLabel: wontfix 15 | 16 | # Comment to post when marking an issue as stale. Set to `false` to disable 17 | markComment: > 18 | This issue has been automatically marked as stale because it has not had 19 | recent activity. It will be closed if no further activity occurs. Thank you 20 | for your contributions. 21 | 22 | # Comment to post when closing a stale issue. Set to `false` to disable 23 | closeComment: false 24 | -------------------------------------------------------------------------------- /pages/getting-started/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Livemark doesn't require any configuration by default. If you haven't started your first project yet you can skip this section. On the other hand, this knowledge will help you later so we recommend reading it in-advance. 4 | 5 | ## Project 6 | 7 | To configure the whole project, you can use `livemark.yaml` file in the project root directory. This file needs to be written in YAML syntax: 8 | 9 | > livemark.yaml 10 | 11 | ```yaml 12 | site: 13 | title: Global Title 14 | ``` 15 | 16 | ## Document 17 | 18 | Every document can be configured using frontmatter. The Document config has a higher priority over the Project configuration. 19 | 20 | > index.md 21 | 22 | ```md 23 | --- 24 | site: 25 | title: My Title 26 | --- 27 | 28 | # My Document 29 | ``` 30 | -------------------------------------------------------------------------------- /livemark/plugins/news/plugin.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from ...plugin import Plugin 3 | 4 | 5 | class NewsPlugin(Plugin): 6 | identity = "news" 7 | validity = { 8 | "type": "object", 9 | "required": ["text"], 10 | "properties": { 11 | "text": {"type": "string"}, 12 | }, 13 | } 14 | 15 | # Context 16 | 17 | @property 18 | def text(self): 19 | return self.config.get("text") 20 | 21 | @property 22 | def code(self): 23 | hash = hashlib.md5() 24 | hash.update(self.text.encode("utf-8")) 25 | code = hash.hexdigest() 26 | return code 27 | 28 | # Process 29 | 30 | def process_markup(self, markup): 31 | if self.text: 32 | markup.add_style("style.css") 33 | markup.add_markup("markup.html") 34 | -------------------------------------------------------------------------------- /tests/test_snippet.py: -------------------------------------------------------------------------------- 1 | from livemark import Snippet 2 | 3 | 4 | # General 5 | 6 | 7 | def test_snippet(): 8 | snippet = Snippet("input", header=["python", "script"]) 9 | assert snippet.header == ["python", "script"] 10 | assert snippet.lang == "python" 11 | assert snippet.type == "script" 12 | assert snippet.props == {} 13 | assert snippet.input == "input" 14 | assert snippet.output is None 15 | 16 | 17 | def test_snippet_update_output(): 18 | snippet = Snippet("input", header=["python", "script"]) 19 | snippet.output = "output" 20 | assert snippet.output == "output" 21 | 22 | 23 | def test_snippet_props(): 24 | snippet = Snippet("input", header=["python", "script", "name1", "name2=value2"]) 25 | assert snippet.props["name1"] is True 26 | assert snippet.props["name2"] == "value2" 27 | -------------------------------------------------------------------------------- /livemark/plugins/pages/markup.html: -------------------------------------------------------------------------------- 1 |
2 | 22 |
23 | -------------------------------------------------------------------------------- /pages/plugin-system/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | Currently, while we're actively working on a proper API Reference, you can explore main Livemark classes on Github by reading the docstrings attached to code blocks: 4 | 5 | - [Document](https://github.com/frictionlessdata/livemark/blob/main/livemark/document.py) 6 | - [Markup](https://github.com/frictionlessdata/livemark/blob/main/livemark/markup.py) 7 | - [Plugin](https://github.com/frictionlessdata/livemark/blob/main/livemark/plugin.py) 8 | - [Project](https://github.com/frictionlessdata/livemark/blob/main/livemark/project.py) 9 | - [Server](https://github.com/frictionlessdata/livemark/blob/main/livemark/server.py) 10 | - [Snippet](https://github.com/frictionlessdata/livemark/blob/main/livemark/snippet.py) 11 | - [System](https://github.com/frictionlessdata/livemark/blob/main/livemark/system.py) 12 | -------------------------------------------------------------------------------- /livemark/plugins/about/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | 3 | 4 | class AboutPlugin(Plugin): 5 | identity = "about" 6 | priority = 20 7 | validity = { 8 | "type": "object", 9 | "properties": { 10 | "text": {"type": "string"}, 11 | }, 12 | } 13 | 14 | # Context 15 | 16 | @property 17 | def text(self): 18 | site = self.document.get_plugin("site") 19 | text = self.config.get("text") 20 | if not text: 21 | if site.description: 22 | text = site.description.split(". ")[0] 23 | return text 24 | 25 | # Process 26 | 27 | def process_markup(self, markup): 28 | if self.document.path == "index": 29 | markup.add_style("style.css") 30 | markup.add_markup("markup.html", target="#livemark-right") 31 | -------------------------------------------------------------------------------- /livemark/plugins/counter/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | 3 | 4 | class CounterPlugin(Plugin): 5 | identity = "counter" 6 | validity = { 7 | "type": "object", 8 | "required": ["type", "code"], 9 | "properties": { 10 | "type": {"type": "string"}, 11 | "code": {"type": "string"}, 12 | }, 13 | } 14 | 15 | # Context 16 | 17 | @property 18 | def type(self): 19 | return self.config.get("type") 20 | 21 | @property 22 | def code(self): 23 | return self.config.get("code") 24 | 25 | # Process 26 | 27 | def process_markup(self, markup): 28 | if self.type == "google": 29 | markup.add_markup("google.html", target="head") 30 | elif self.type == "plausible": 31 | markup.add_markup("plausible.html", target="head") 32 | -------------------------------------------------------------------------------- /livemark/plugins/cards/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | const handlePopstate = async () => { 3 | const href = location.hash; 4 | if (href.startsWith("#card=")) { 5 | const code = href.split("=")[1]; 6 | const response = await fetch(`/assets/cards/${code}.html`); 7 | const html = await response.text(); 8 | $("#livemark-cards .modal-title").html(""); 9 | $("#livemark-cards .modal-body").html(html); 10 | $("#livemark-cards h1").appendTo("#livemark-cards .modal-title"); 11 | $("#livemark-cards .modal").modal(); 12 | $("#livemark-cards .modal").on("hidden.bs.modal", () => { 13 | history.pushState("", document.title, window.location.pathname); 14 | }); 15 | } 16 | }; 17 | window.addEventListener("popstate", handlePopstate); 18 | handlePopstate(); 19 | }); 20 | -------------------------------------------------------------------------------- /livemark/plugins/comments/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | 3 | 4 | class CommentsPlugin(Plugin): 5 | identity = "comments" 6 | priority = 45 7 | validity = { 8 | "type": "object", 9 | "properties": { 10 | "enable": {"type": "boolean"}, 11 | }, 12 | } 13 | 14 | # Context 15 | 16 | @property 17 | def code(self): 18 | return self.config.get("code") 19 | 20 | @property 21 | def link(self): 22 | return self.config.get("link") 23 | 24 | # Process 25 | 26 | def process_markup(self, markup): 27 | if self.code and self.link: 28 | markup.add_style("style.css") 29 | markup.add_script("https://livemark.disqus.com/count.js") 30 | markup.add_script("script.js") 31 | markup.add_markup("markup.html", target="#livemark-main") 32 | -------------------------------------------------------------------------------- /tests/program/test_main.py: -------------------------------------------------------------------------------- 1 | from typer.testing import CliRunner 2 | from livemark import program, __version__ 3 | 4 | runner = CliRunner() 5 | 6 | 7 | # General 8 | 9 | 10 | def test_program(): 11 | result = runner.invoke(program) 12 | assert result.exit_code == 2 13 | assert result.stdout.count("Usage") 14 | 15 | 16 | def test_program_version(): 17 | result = runner.invoke(program, "--version") 18 | assert result.exit_code == 0 19 | assert result.stdout.count(__version__) 20 | 21 | 22 | def test_program_help(): 23 | result = runner.invoke(program, "--help") 24 | assert result.exit_code == 0 25 | assert result.stdout.count("Usage") 26 | 27 | 28 | def test_program_error_bad_command(): 29 | result = runner.invoke(program, "bad") 30 | assert result.exit_code == 2 31 | assert result.stdout.count("No such command 'bad'") 32 | -------------------------------------------------------------------------------- /livemark/plugins/display/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | 3 | 4 | # NOTE: 5 | # Consider using pure jQuery instead of ue-scroll 6 | # Consider adding help button to teach how to scroll sidebars, shorcuts, etc 7 | 8 | 9 | class DisplayPlugin(Plugin): 10 | identity = "display" 11 | validity = { 12 | "type": "object", 13 | "properties": { 14 | "speed": {"type": "integer"}, 15 | }, 16 | } 17 | 18 | # Context 19 | 20 | @property 21 | def speed(self): 22 | return self.config.get("speed", 10) 23 | 24 | # Process 25 | 26 | def process_markup(self, markup): 27 | url = "https://unpkg.com" 28 | markup.add_style("style.css") 29 | markup.add_script(f"{url}/ue-scroll-js@2.0.2/dist/ue-scroll.min.js") 30 | markup.add_script("script.js") 31 | markup.add_markup("markup.html", target="body") 32 | -------------------------------------------------------------------------------- /livemark/plugins/pipeline/plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | from ...plugin import Plugin 4 | from frictionless import Pipeline 5 | 6 | 7 | class PipelinePlugin(Plugin): 8 | identity = "pipeline" 9 | priority = 60 10 | 11 | # Process 12 | 13 | def process_document(self, document): 14 | self.__count = 0 15 | 16 | def process_snippet(self, snippet): 17 | if self.document.format == "html": 18 | if snippet.type == "pipeline" and snippet.lang in ["yaml", "json"]: 19 | if snippet.lang == "yaml": 20 | spec = yaml.safe_load(str(snippet.input).strip()) 21 | if snippet.lang == "json": 22 | spec = json.loads(str(snippet.input).strip()) 23 | self.__count += 1 24 | pipeline = Pipeline(**spec) 25 | snippet.output = self.read_asset("markup.html", pipeline=pipeline) + "\n" 26 | -------------------------------------------------------------------------------- /pages/plugin-system/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | ## Overview 4 | 5 | In a nutshell, Livemark is just a text processor. It takes a Markdown document and outputs an HTML document. It is also possible to output different formats but the main Livemark specialization is Markdown-to-HTML conversion. 6 | 7 | ## Plugins 8 | 9 | Livemark's core only provides an abstract classe like Project, Document, or Snippet. The actual work is done by various plugins like HtmlPlugin, SitePlugin, or TablePlugin. You can find their description in the Livemark Markdown and Livemark Features sections. 10 | 11 | ## Build 12 | 13 | On the figure below we present Livemark's building flow schematically: a Markdown document containing various snippets being converted to an HTML document containing various markup blocks by Livemark and its plugins: 14 | 15 | ```yaml image 16 | path: ../../assets/flow.png 17 | width: 100% 18 | height: unset 19 | ``` 20 | -------------------------------------------------------------------------------- /pages/release.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | ## Current 4 | 5 | Current Livemark Version: 6 | 7 | ```html markup 8 |

9 | v{{ livemark.__version__ }} 10 | | PyPi 11 |

12 | ``` 13 | 14 | ## History 15 | 16 | This section only describes the most significant changes, including breaking changes. The full changelog and documentation for all released versions can be found in the nicely formatted [commit history](https://github.com/frictionlessdata/livemark/commits/main). 17 | 18 | ### v0.100 19 | 20 | - Implemented Reference plugin 21 | - Remove Python3.6 and Python3.7 support 22 | 23 | ### v0.95 24 | 25 | - Implemented Columns plugin 26 | - Removed support of `livemark-markdown` class 27 | 28 | ### v0.94 29 | 30 | - Implemented Tabs plugin 31 | 32 | ### v0.80 33 | 34 | - Rebased `table` from HandOnTable to DataTables backend 35 | -------------------------------------------------------------------------------- /livemark/plugins/links/style.css: -------------------------------------------------------------------------------- 1 | #livemark-links { 2 | color: #888; 3 | } 4 | 5 | #livemark-links ul { 6 | overflow: hidden; 7 | position: relative; 8 | padding-left: 0; 9 | margin: 0; 10 | } 11 | 12 | #livemark-links li { 13 | list-style: none; 14 | } 15 | 16 | #livemark-links a { 17 | display: inline-block; 18 | color: currentColor; 19 | position: relative; 20 | width: 100%; 21 | line-height: 100%; 22 | padding-top: 5px; 23 | padding-bottom: 5px; 24 | } 25 | 26 | #livemark-links a:hover { 27 | color: #80b2e6; 28 | } 29 | 30 | #livemark-links a[target="_blank"]:after { 31 | content: "\f35d"; 32 | font-family: "Font Awesome 5 Free"; 33 | font-weight: 900; 34 | vertical-align: text-top; 35 | text-decoration: none; 36 | display: inline-block; 37 | color: #aaa; 38 | font-size: 12px; 39 | margin-left: -2px; 40 | } 41 | 42 | #livemark-links a[target="_blank"]:hover:after { 43 | color: #80b2e6; 44 | } 45 | -------------------------------------------------------------------------------- /livemark/plugins/cards/plugin.py: -------------------------------------------------------------------------------- 1 | from ...project import Project 2 | from ...plugin import Plugin 3 | from ... import helpers 4 | 5 | 6 | class CardsPlugin(Plugin): 7 | identity = "cards" 8 | 9 | # Process 10 | 11 | def process_markup(self, markup): 12 | markup.add_style("style.css") 13 | markup.add_script("script.js") 14 | markup.add_markup("markup.html") 15 | 16 | # Helpers 17 | 18 | @staticmethod 19 | def create_card(source, *, code, **context): 20 | target = f"assets/cards/{code}.html" 21 | project = Project(source, target=target, config={"site": False}) 22 | project.document.read() 23 | project.document.get_plugin("logic").context.update(code=code) 24 | project.document.get_plugin("logic").context.update(context) 25 | project.document.build() 26 | 27 | @staticmethod 28 | def delete_cards(): 29 | target = "assets/cards" 30 | helpers.remove_dir(target) 31 | -------------------------------------------------------------------------------- /livemark/plugins/logic/plugin.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import frictionless 3 | from jinja2 import Environment, FileSystemLoader 4 | from ...plugin import Plugin 5 | 6 | 7 | class LogicPlugin(Plugin): 8 | identity = "logic" 9 | priority = 80 10 | 11 | def __init__(self, document): 12 | super().__init__(document) 13 | 14 | # Prepare context 15 | self.__context = {} 16 | self.__context["document"] = self.document 17 | self.__context["livemark"] = importlib.import_module("livemark") 18 | self.__context["frictionless"] = frictionless 19 | 20 | # Context 21 | 22 | @property 23 | def context(self): 24 | return self.__context 25 | 26 | # Process 27 | 28 | def process_document(self, document): 29 | environ = Environment(loader=FileSystemLoader("."), trim_blocks=True) 30 | template = environ.from_string(document.content) 31 | document.content = template.render(**self.context) 32 | -------------------------------------------------------------------------------- /livemark/plugins/topics/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | from ... import errors 3 | 4 | 5 | class TopicsPlugin(Plugin): 6 | identity = "topics" 7 | priority = 20 8 | validity = { 9 | "type": "object", 10 | "properties": { 11 | "selector": {"type": "string"}, 12 | }, 13 | } 14 | 15 | # Context 16 | 17 | @property 18 | def selector(self): 19 | selector = self.config.get("selector", "h2, h3") 20 | if len(selector.split(",")) > 2: 21 | raise errors.Error("Maximum topic levels is 2") 22 | return selector 23 | 24 | # Process 25 | 26 | def process_markup(self, markup): 27 | if self.document.path != "index": 28 | markup.add_style("style.css") 29 | markup.add_script("https://unpkg.com/tocbot@4.12.3/dist/tocbot.min.js") 30 | markup.add_script("script.js") 31 | markup.add_markup("markup.html", target="#livemark-right") 32 | -------------------------------------------------------------------------------- /livemark/plugins/rating/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | 3 | 4 | class RatingPlugin(Plugin): 5 | identity = "rating" 6 | priority = 30 7 | validity = { 8 | "type": "object", 9 | "properties": { 10 | "type": {"type": "string"}, 11 | }, 12 | } 13 | 14 | # Context 15 | 16 | @property 17 | def type(self): 18 | return self.config.get("type", "star") 19 | 20 | @property 21 | def user(self): 22 | github = self.document.get_plugin("github") 23 | if github: 24 | return github.user 25 | 26 | @property 27 | def repo(self): 28 | github = self.document.get_plugin("github") 29 | if github: 30 | return github.repo 31 | 32 | # Process 33 | 34 | def process_markup(self, markup): 35 | if self.user and self.repo: 36 | markup.add_style("style.css") 37 | markup.add_markup("markup.html", target="#livemark-right") 38 | -------------------------------------------------------------------------------- /livemark/plugins/notes/plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | from ...plugin import Plugin 4 | 5 | 6 | class NotesPlugin(Plugin): 7 | identity = "notes" 8 | priority = 50 9 | validity = { 10 | "type": "object", 11 | "properties": { 12 | "format": {"type": "string"}, 13 | }, 14 | } 15 | 16 | # Context 17 | 18 | @property 19 | def format(self): 20 | return self.config.get("format", "%Y-%m-%d %H:%M") 21 | 22 | @property 23 | def current(self): 24 | return datetime.fromtimestamp(os.path.getmtime(self.document.source)) 25 | 26 | @property 27 | def edit_url(self): 28 | github = self.document.get_plugin("github") 29 | if github: 30 | return github.edit_url 31 | 32 | # Process 33 | 34 | def process_markup(self, markup): 35 | markup.add_style("style.css") 36 | markup.add_markup("markup.html", target="#livemark-main", action="prepend") 37 | -------------------------------------------------------------------------------- /livemark/plugins/package/plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | from ...plugin import Plugin 4 | from frictionless import Package 5 | 6 | 7 | # NOTE: 8 | # We need to render description's markdown 9 | 10 | 11 | class PackagePlugin(Plugin): 12 | identity = "package" 13 | priority = 60 14 | 15 | # Process 16 | 17 | def process_document(self, document): 18 | self.__count = 0 19 | 20 | def process_snippet(self, snippet): 21 | if self.document.format == "html": 22 | if snippet.type == "package" and snippet.lang in ["yaml", "json"]: 23 | if snippet.lang == "yaml": 24 | spec = yaml.safe_load(str(snippet.input).strip()) 25 | if snippet.lang == "json": 26 | spec = json.loads(str(snippet.input).strip()) 27 | self.__count += 1 28 | package = Package(**spec) 29 | snippet.output = self.read_asset("markup.html", package=package) + "\n" 30 | -------------------------------------------------------------------------------- /livemark/plugins/resource/plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | from ...plugin import Plugin 4 | from frictionless import Resource 5 | 6 | 7 | # NOTE: 8 | # We need to render description's markdown 9 | 10 | 11 | class ResourcePlugin(Plugin): 12 | identity = "resource" 13 | priority = 60 14 | 15 | # Process 16 | 17 | def process_document(self, document): 18 | self.__count = 0 19 | 20 | def process_snippet(self, snippet): 21 | if self.document.format == "html": 22 | if snippet.type == "resource" and snippet.lang in ["yaml", "json"]: 23 | if snippet.lang == "yaml": 24 | spec = yaml.safe_load(str(snippet.input).strip()) 25 | if snippet.lang == "json": 26 | spec = json.loads(str(snippet.input).strip()) 27 | self.__count += 1 28 | resource = Resource(**spec) 29 | snippet.output = self.read_asset("markup.html", resource=resource) + "\n" 30 | -------------------------------------------------------------------------------- /pages/universe/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | ```html markup 4 |
5 | {% for row in frictionless.Resource('data/plugins.csv').read_rows() %} 6 |
7 |
8 |

9 | {{ row.repo }} 10 | 11 |

12 |

{{ row.description or 'Description is not provided'}}

13 |

14 | 15 | Github 16 | 17 |

18 |
19 |
20 | 21 | 22 | {{ row.stars }} 23 | 24 |
25 |
26 | {% endfor %} 27 |
28 | ``` 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Livemark has been created by the Frictionless Data team and is open for contributing for anyone who is interested. Note: We're about to migrate this guide to using `livemark run` but, for now, you need to have `make` command installed or use underlaying command written in Makefile. 4 | 5 | ## Prepare 6 | 7 | To start working on the project clone the repository and enter its directory: 8 | 9 | ```bash 10 | $ git clone git@github.com:frictionlessdata/livemark.git 11 | $ cd livemark 12 | ``` 13 | 14 | Create a virtual environment (optional): 15 | 16 | ```bash 17 | $ python3 -m venv .python 18 | $ source .python/bin/activate 19 | ``` 20 | 21 | And install dependencies: 22 | 23 | ``` 24 | $ make install 25 | ``` 26 | 27 | ## Testing 28 | 29 | We use Pytest for writing tests and Pylama for linting: 30 | 31 | ```bash 32 | $ make test 33 | ``` 34 | 35 | ## Releasing 36 | 37 | Update the version in `livemark/assets/VERSION` and run: 38 | 39 | ```bash 40 | $ make release 41 | ``` 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all install format github lint release test 2 | 3 | 4 | PACKAGE := $(shell grep '^PACKAGE =' setup.py | cut -d '"' -f2) 5 | VERSION := $(shell head -n 1 $(PACKAGE)/assets/VERSION) 6 | LEAD := $(shell head -n 1 LEAD.md) 7 | 8 | 9 | all: 10 | @grep '^\.PHONY' Makefile | cut -d' ' -f2- | tr ' ' '\n' 11 | 12 | format: 13 | black $(PACKAGE) tests 14 | 15 | install: 16 | pip install --upgrade -e .[dev] 17 | 18 | lint: 19 | black $(PACKAGE) tests --check 20 | pylama $(PACKAGE) tests 21 | # mypy $(PACKAGE) --ignore-missing-imports 22 | 23 | release: 24 | git checkout main && git pull origin && git fetch -p 25 | @git log --pretty=format:"%C(yellow)%h%Creset %s%Cgreen%d" --reverse -20 26 | @echo "\nReleasing v$(VERSION) in 10 seconds. Press to abort\n" && sleep 10 27 | make test && git commit -a -m 'v$(VERSION)' && git tag -a v$(VERSION) -m 'v$(VERSION)' 28 | git push --follow-tags 29 | 30 | test: 31 | make lint 32 | pytest --cov ${PACKAGE} --cov-report term-missing --cov-fail-under 10 33 | -------------------------------------------------------------------------------- /pages/universe/projects.md: -------------------------------------------------------------------------------- 1 | # Projects 2 | 3 | ```html markup 4 |
5 | {% for row in frictionless.Resource('data/livemarks.csv').read_rows() %} 6 |
7 |
8 |

9 | {{ row.repo }} 10 | 11 |

12 |

{{ row.description or 'Description is not provided'}}

13 |

14 | 15 | Github 16 | 17 |

18 |
19 |
20 | 21 | 22 | {{ row.stars }} 23 | 24 |
25 |
26 | {% endfor %} 27 |
28 | ``` 29 | -------------------------------------------------------------------------------- /livemark/program/common.py: -------------------------------------------------------------------------------- 1 | from typer import Argument, Option 2 | from .. import settings 3 | 4 | 5 | # General 6 | 7 | 8 | source = Argument( 9 | None, 10 | help="Path to the source file", 11 | ) 12 | 13 | target = Option( 14 | None, 15 | help="Path to the target file", 16 | ) 17 | 18 | format = Option( 19 | None, 20 | help="Format of the target file", 21 | ) 22 | 23 | config = Option( 24 | None, 25 | help="Path to a config file", 26 | ) 27 | 28 | host = Option( 29 | settings.DEFAULT_HOST, 30 | help="Server host", 31 | ) 32 | 33 | port = Option( 34 | settings.DEFAULT_PORT, 35 | help="Server port", 36 | ) 37 | 38 | task = Argument( 39 | None, 40 | help="A task id", 41 | ) 42 | 43 | 44 | # Command 45 | 46 | 47 | live = Option( 48 | default=False, 49 | help="Live mode", 50 | ) 51 | 52 | 53 | diff = Option( 54 | default=False, 55 | help="Return the diff", 56 | ) 57 | 58 | print = Option( 59 | False, 60 | help="Return the document", 61 | ) 62 | -------------------------------------------------------------------------------- /livemark/plugins/columns/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | 3 | 4 | class ColumnsPlugin(Plugin): 5 | identity = "columns" 6 | 7 | # Process 8 | 9 | def process_markup(self, markup): 10 | if self.document.format == "html": 11 | # Collect sources 12 | number = 1 13 | groups = {} 14 | sources = list(markup.query("[data-columns]").items()) 15 | for source in sources: 16 | column = dict(content=source.html(), markup=source) 17 | groups.setdefault(number, []) 18 | groups[number].append(column) 19 | if not source.next().attr("data-columns"): 20 | number += 1 21 | 22 | # Render sources 23 | for columns in groups.values(): 24 | output = self.read_asset("markup.html", columns=columns) 25 | columns[0]["markup"].after(output) 26 | 27 | # Delete sources 28 | markup.query("[data-columns]").remove() 29 | -------------------------------------------------------------------------------- /pages/plugin-system/adding-plugin.md: -------------------------------------------------------------------------------- 1 | # Adding a Plugin 2 | 3 | ## Overview 4 | 5 | When you work on your Livemark project there are different options of adding a plugin to the building process: 6 | - installing an external plugin 7 | - adding a custom plugin 8 | 9 | ## External Plugin 10 | 11 | You can install an external plugin, for example: 12 | 13 | ```bash 14 | $ pip install livemark-ckan 15 | ``` 16 | 17 | And activate it in `livemark.yaml` file: 18 | 19 | ```yaml 20 | ckan: true # or a plugin config 21 | ``` 22 | 23 | ## Custom Plugin 24 | 25 | You can [Write a Plugin](write-plugin.html) and put it into `plugin.py` module or export from `plugins` package in your project root. You don't need to activate it in `livemark.yaml`. For example, you can take a look how it works in the [Livemark Project Template](https://github.com/frictionlessdata/livemark-project). 26 | 27 | It's also possible to add a plugin programmatically: 28 | 29 | ```python 30 | from livemark import system 31 | 32 | system.register(CustomPlugin) 33 | ``` 34 | -------------------------------------------------------------------------------- /pages/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ```yaml remark 4 | type: danger 5 | text: Livemark executes code in documents that you build. It means that you MUST never build/start any Livemark projects from untrusted sources. Treat any Livemark project as you treat Python or Bash scripts security-wise 6 | ``` 7 | 8 | Livemark is a Python library that works on Windown, MacOs, and Linux. It uses SemVer for semantic versioning. Please file an [issue](https://github.com/frictionlessdata/livemark/issues) if you run into any problems during installation. 9 | 10 | ## Install 11 | 12 | Livemark can be installed with pip (or [pipx](https://pypa.github.io/pipx/)): 13 | 14 | ```bash 15 | $ pip install livemark 16 | ``` 17 | 18 | After installation, you can start writing your document (eg an `index.md` file) using the extended Markdown syntax described in the next sections. 19 | 20 | ## Verify 21 | 22 | To make sure that Livemark is installed correctly on your machine: 23 | 24 | ```bash 25 | $ livemark --version 26 | ``` 27 | ``` 28 | 1.0.0 29 | ``` 30 | -------------------------------------------------------------------------------- /livemark/plugins/brand/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | from ... import helpers 3 | 4 | 5 | class BrandPlugin(Plugin): 6 | identity = "brand" 7 | priority = 80 8 | validity = { 9 | "type": "object", 10 | "properties": { 11 | "text": {"type": "string"}, 12 | }, 13 | } 14 | 15 | # Context 16 | 17 | @property 18 | def path(self): 19 | return helpers.get_url_relpath(".", self.document.path) 20 | 21 | @property 22 | def text(self): 23 | site = self.document.get_plugin("site") 24 | return self.config.get("text", site.title) 25 | 26 | @property 27 | def title_extra(self): 28 | site = self.document.get_plugin("site") 29 | if self.text != site.title: 30 | return f" | {self.text}" 31 | 32 | # Process 33 | 34 | def process_markup(self, markup): 35 | markup.add_style("style.css") 36 | markup.add_markup("markup.html", target="#livemark-left") 37 | if self.title_extra: 38 | markup.query("title").append(self.title_extra) 39 | -------------------------------------------------------------------------------- /livemark/plugins/site/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | const content = document.querySelector("#livemark-main"); 3 | const headings = content.querySelectorAll("h1, h2, h3, h4, h5, h6, h7"); 4 | const headingMap = {}; 5 | 6 | // Add identifiers 7 | Array.prototype.forEach.call(headings, function (heading) { 8 | const id = heading.id 9 | ? heading.id 10 | : heading.textContent 11 | .trim() 12 | .toLowerCase() 13 | .split(" ") 14 | .join("-") 15 | .replace(/[!@#$%^&*():]/gi, "") 16 | .replace(/\//gi, "-"); 17 | headingMap[id] = !isNaN(headingMap[id]) ? ++headingMap[id] : 0; 18 | if (headingMap[id]) { 19 | heading.id = id + "-" + headingMap[id]; 20 | } else { 21 | heading.id = id; 22 | } 23 | }); 24 | 25 | // Add links 26 | Array.prototype.forEach.call(headings, function (heading) { 27 | const link = document.createElement("a"); 28 | link.href = "#" + heading.id; 29 | link.innerText = "#"; 30 | link.classList.add("heading"); 31 | heading.appendChild(link); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Open Knowledge Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /livemark/plugins/tabs/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | 3 | 4 | class TabsPlugin(Plugin): 5 | identity = "tabs" 6 | 7 | # Process 8 | 9 | def process_markup(self, markup): 10 | if self.document.format == "html": 11 | markup.add_style("style.css") 12 | 13 | # Collect sources 14 | number = 1 15 | groups = {} 16 | sources = list(markup.query("[data-tabs]").items()) 17 | for source in sources: 18 | name = source.attr("data-tabs") 19 | id = f"livemark-tabs-{number}-{name}" 20 | tab = dict(id=id, name=name, content=source.html(), markup=source) 21 | groups.setdefault(number, []) 22 | groups[number].append(tab) 23 | if not source.next().attr("data-tabs"): 24 | number += 1 25 | 26 | # Render sources 27 | for tabs in groups.values(): 28 | output = self.read_asset("markup.html", tabs=tabs) 29 | tabs[0]["markup"].after(output) 30 | 31 | # Delete sources 32 | markup.query("[data-tabs]").remove() 33 | -------------------------------------------------------------------------------- /pages/feature-reference/blogging.md: -------------------------------------------------------------------------------- 1 | # Blogging 2 | 3 | ## Blog 4 | 5 | Blog is an essential part of many websites and Livemark provides it as well. To activate the blog feature: 6 | - create a `blog` folder in the project's root directory 7 | - add an article there, e.g. `blog/2021-09-01-article.md` 8 | - add a blog index to the pages, e.g. `- path: blog/index` 9 | 10 | It's possible to customize an article using frontmatter: 11 | 12 | > blog/2021-09-01-article.md 13 | 14 | ```yaml 15 | blog: 16 | author: John Doe 17 | image: ../assets/example.png 18 | ``` 19 | 20 | See [Blog](../../blog/index.html) as an example. 21 | 22 | ## Comments 23 | 24 | To enable comments for a specific article you need to provide frontmatter with Disqus id and a canonical website link: 25 | 26 | > article.md 27 | 28 | ```yaml 29 | comments: 30 | code: livemark 31 | link: https://livemark.frictionlessdata.io 32 | ``` 33 | 34 | It's possible to enable comments for all the pages using project config: 35 | 36 | > livemark.yaml 37 | 38 | ```yaml 39 | comments: 40 | code: livemark 41 | link: https://livemark.frictionlessdata.io 42 | ``` 43 | 44 | See [Forum](../forum.html) as an example. 45 | -------------------------------------------------------------------------------- /livemark/plugins/map/plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | from ...plugin import Plugin 4 | from ... import helpers 5 | 6 | 7 | class MapPlugin(Plugin): 8 | identity = "map" 9 | priority = 60 10 | 11 | # Process 12 | 13 | def process_document(self, document): 14 | self.__count = 0 15 | 16 | def process_snippet(self, snippet): 17 | if self.document.format == "html": 18 | if snippet.type == "map" and snippet.lang == "yaml": 19 | spec_yaml = str(snippet.input).strip() 20 | spec_python = yaml.safe_load(spec_yaml) 21 | spec = json.dumps(json.loads(helpers.read_file(spec_python["data"]))) 22 | spec = spec.replace("'", "\\'") 23 | self.__count += 1 24 | map = {"spec": spec, "elem": f"livemark-map-{self.__count}"} 25 | snippet.output = self.read_asset("markup.html", map=map) + "\n" 26 | 27 | def process_markup(self, markup): 28 | if self.__count: 29 | url = "https://unpkg.com" 30 | markup.add_style(f"{url}/leaflet@1.7.1/dist/leaflet.css") 31 | markup.add_script(f"{url}/leaflet@1.7.1/dist/leaflet.js") 32 | -------------------------------------------------------------------------------- /livemark/plugins/pages/style.css: -------------------------------------------------------------------------------- 1 | #livemark-pages { 2 | color: #888; 3 | } 4 | 5 | #livemark-pages ul { 6 | overflow: hidden; 7 | position: relative; 8 | padding-left: 0; 9 | margin: 0; 10 | } 11 | 12 | #livemark-pages ul.secondary { 13 | margin-left: 20px; 14 | display: none; 15 | } 16 | 17 | #livemark-pages li.active ul.secondary { 18 | display: block; 19 | } 20 | 21 | #livemark-pages li { 22 | list-style: none; 23 | } 24 | 25 | #livemark-pages a { 26 | display: inline-block; 27 | color: currentColor; 28 | position: relative; 29 | width: 100%; 30 | line-height: 100%; 31 | padding-top: 5px; 32 | padding-bottom: 5px; 33 | } 34 | 35 | #livemark-pages li.active > a { 36 | font-weight: 700; 37 | } 38 | 39 | #livemark-pages li.group.active > a { 40 | font-weight: normal; 41 | } 42 | 43 | #livemark-pages li.group a.primary::after { 44 | content: "\f054"; 45 | font-size: 16px; 46 | font-family: "Font Awesome 5 Free"; 47 | font-weight: 900; 48 | position: absolute; 49 | top: 2px; 50 | right: 0px; 51 | } 52 | 53 | #livemark-pages li.group.active a.primary::after { 54 | content: "\f078"; 55 | } 56 | 57 | #livemark-pages a:hover { 58 | color: #80b2e6; 59 | } 60 | -------------------------------------------------------------------------------- /livemark/plugins/table/markup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | {% for column in columns %} 7 | 8 | {% endfor %} 9 | 10 | 11 | 12 | {% for row in rows %} 13 | 14 | {% for column in columns %} 15 | 16 | {% endfor %} 17 | 18 | {% endfor %} 19 | 20 | 21 | 22 | {% for column in columns %} 23 | 24 | {% endfor %} 25 | 26 | 27 |
{{ column.title or column.name or column.data }}
{{ row[column.data] if row[column.data] is not none else '' }}
{{ column.title or column.name or column.data }}
28 |
29 |
30 | {% if card %} 31 | 34 | {% else %} 35 | 40 | {% endif %} 41 | -------------------------------------------------------------------------------- /pages/plugin-system/writing-plugin.md: -------------------------------------------------------------------------------- 1 | # Writing a Plugin 2 | 3 | > Start quickly with [Livemark Plugin](https://github.com/frictionlessdata/livemark-plugin) Github Template 4 | 5 | ## Overview 6 | 7 | Livemark provides a plugin interface to help write new plugins. There are 4 main hooks a plugin author can use to alter the rendering process. All of them take an corresponding object that can be updated: 8 | 9 | - `Pluing.process_project(project)` 10 | - `pluing.process_document(document)` 11 | - `pluing.process_snippet(snippet)` 12 | - `pluing.process_markup(markup)` 13 | 14 | ## Example 15 | 16 | This plugin simply adds a string to H1 tags on every page in the project: 17 | 18 | ```python 19 | from livemark import Plugin 20 | 21 | 22 | class CustomPlugin(Plugin): 23 | identity = "custom" 24 | 25 | # Process 26 | 27 | def process_markup(self, markup): 28 | markup.add_markup("(template)", target="h1") 29 | ``` 30 | 31 | ## References 32 | 33 | To help you write a plugin, explore core plugins, architecture, and API References: 34 | - [Core Plugins](https://github.com/frictionlessdata/livemark/tree/main/livemark/plugins) 35 | - [Architecture](architecture.html) 36 | - [API Reference](reference.html) 37 | -------------------------------------------------------------------------------- /livemark/plugins/remark/plugin.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import marko 3 | from pyquery import PyQuery 4 | from marko.ext.gfm import GFM 5 | from ...plugin import Plugin 6 | 7 | 8 | class RemarkPlugin(Plugin): 9 | identity = "remark" 10 | 11 | # Process 12 | 13 | def process_snippet(self, snippet): 14 | if self.document.format == "html": 15 | if snippet.type == "remark": 16 | if snippet.lang == "yaml": 17 | context = yaml.safe_load(str(snippet.input).strip()) 18 | snippet.output = self.read_asset("markup.html", **context) 19 | elif snippet.lang in ["markdown", "html"]: 20 | type = snippet.props.get("type", "warning") 21 | text = snippet.input 22 | if snippet.lang == "markdown": 23 | markdown = marko.Markdown() 24 | markdown.use(GFM) 25 | text = PyQuery(markdown.convert(snippet.input)).html() 26 | context = dict(type=type, text=text) 27 | snippet.output = self.read_asset("markup.html", **context) 28 | 29 | def process_markup(self, markup): 30 | markup.add_style("style.css") 31 | -------------------------------------------------------------------------------- /livemark/plugins/display/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | // Init 3 | const readability = localStorage.getItem("livemark-display-readability"); 4 | if (readability === "plus") { 5 | document.body.classList.add("with-readability"); 6 | } else { 7 | document.body.classList.remove("with-readability"); 8 | } 9 | 10 | // Plus 11 | document 12 | .getElementById("livemark-display-plus") 13 | .addEventListener("click", function () { 14 | document.body.classList.add("with-readability"); 15 | localStorage.setItem("livemark-display-readability", "plus"); 16 | }); 17 | 18 | // Minus 19 | document 20 | .getElementById("livemark-display-minus") 21 | .addEventListener("click", function () { 22 | document.body.classList.remove("with-readability"); 23 | localStorage.setItem("livemark-display-readability", "minus"); 24 | }); 25 | 26 | // Print 27 | document 28 | .getElementById("livemark-display-print") 29 | .addEventListener("click", function () { 30 | window.print(); 31 | }); 32 | 33 | // Scroll 34 | const scrollSpeed = parseInt("{{ plugin.speed }}"); 35 | UeScroll.init({ element: "#livemark-display-scroll .fa", scrollSpeed }); 36 | }); 37 | -------------------------------------------------------------------------------- /livemark/plugins/reference/plugin.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from .reference import Reference 3 | from ...plugin import Plugin 4 | from ... import errors 5 | 6 | 7 | # NOTE: 8 | # We need to render long_description markdown 9 | # We'd like to be able to process it even for a markdown target (as scripts) 10 | # To achieve it we need to update the protocol that HtmlRenderer uses for snippets 11 | 12 | 13 | class ReferencePlugin(Plugin): 14 | identity = "reference" 15 | priority = 60 16 | 17 | # Process 18 | 19 | def process_snippet(self, snippet): 20 | if snippet.type == "reference" and snippet.lang == "yaml": 21 | spec = yaml.safe_load(str(snippet.input).strip()) 22 | references = [] 23 | for pointer in spec.get("references", []): 24 | reference = Reference.from_name(pointer) 25 | references.append(reference) 26 | if not references: 27 | raise errors.Error(f"No references found: {spec}") 28 | context = {} 29 | context["references"] = references 30 | context["level"] = spec.get("level", 3) 31 | snippet.output = self.read_asset("markup.html", **context) 32 | 33 | def process_markup(self, markup): 34 | markup.add_style("style.css") 35 | -------------------------------------------------------------------------------- /livemark/plugins/search/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | from ... import helpers 3 | 4 | 5 | class SearchPlugin(Plugin): 6 | identity = "search" 7 | 8 | # Context 9 | 10 | @property 11 | def items(self): 12 | items = [] 13 | documents = [self.document] 14 | if self.document.project: 15 | documents = self.document.project.documents 16 | for document in documents: 17 | item = {} 18 | item["name"] = document.get_plugin("site").name 19 | item["path"] = document.path 20 | item["relpath"] = helpers.get_url_relpath(document.path, self.document.path) 21 | item["text"] = document.content 22 | items.append(item) 23 | return items 24 | 25 | # Process 26 | 27 | def process_markup(self, markup): 28 | if self.items: 29 | url = "https://unpkg.com" 30 | markup.add_style("style.css") 31 | markup.add_script(f"{url}/lunr@2.3.9/lunr.min.js") 32 | markup.add_script(f"{url}/jquery-highlight@3.5.0/jquery.highlight.js") 33 | markup.add_script(f"{url}/jquery.scrollto@2.1.3/jquery.scrollTo.js") 34 | markup.add_script("script.js") 35 | markup.add_markup("markup.html", target="body") 36 | -------------------------------------------------------------------------------- /livemark/plugins/video/plugin.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from ...plugin import Plugin 3 | 4 | 5 | class VideoPlugin(Plugin): 6 | identity = "video" 7 | validity = { 8 | "type": "object", 9 | "properties": { 10 | "width": {"type": "number"}, 11 | "height": {"type": "number"}, 12 | }, 13 | } 14 | 15 | # Context 16 | 17 | @property 18 | def width(self): 19 | return self.config.get("width", 600) 20 | 21 | @property 22 | def height(self): 23 | return self.config.get("height", 400) 24 | 25 | # Process 26 | 27 | def process_snippet(self, snippet): 28 | if self.document.format == "html": 29 | if snippet.type.startswith("video") and snippet.lang == "yaml": 30 | context = yaml.safe_load(str(snippet.input).strip()) 31 | context.setdefault("width", self.width) 32 | context.setdefault("height", self.height) 33 | if snippet.type == "video": 34 | snippet.output = self.read_asset("base.html", **context) 35 | elif snippet.type == "video/youtube": 36 | snippet.output = self.read_asset("youtube.html", **context) 37 | 38 | def process_markup(self, markup): 39 | markup.add_style("style.css") 40 | -------------------------------------------------------------------------------- /livemark/plugins/audio/plugin.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from ...plugin import Plugin 3 | 4 | 5 | class AudioPlugin(Plugin): 6 | identity = "audio" 7 | validity = { 8 | "type": "object", 9 | "properties": { 10 | "width": {"type": "number"}, 11 | "height": {"type": "number"}, 12 | }, 13 | } 14 | 15 | # Context 16 | 17 | @property 18 | def width(self): 19 | return self.config.get("width", 600) 20 | 21 | @property 22 | def height(self): 23 | return self.config.get("height", 400) 24 | 25 | # Process 26 | 27 | def process_snippet(self, snippet): 28 | if self.document.format == "html": 29 | if snippet.type.startswith("audio") and snippet.lang == "yaml": 30 | context = yaml.safe_load(str(snippet.input).strip()) 31 | context.setdefault("width", self.width) 32 | context.setdefault("height", self.height) 33 | if snippet.type == "audio": 34 | snippet.output = self.read_asset("base.html", **context) 35 | elif snippet.type == "audio/soundcloud": 36 | snippet.output = self.read_asset("soundcloud.html", **context) 37 | 38 | def process_markup(self, markup): 39 | markup.add_style("style.css") 40 | -------------------------------------------------------------------------------- /livemark/plugins/image/plugin.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from ...plugin import Plugin 3 | 4 | 5 | class ImagePlugin(Plugin): 6 | identity = "image" 7 | validity = { 8 | "type": "object", 9 | "properties": { 10 | "width": {"type": "number"}, 11 | "height": {"type": "number"}, 12 | }, 13 | } 14 | 15 | # Context 16 | 17 | @property 18 | def width(self): 19 | return self.config.get("width", 600) 20 | 21 | @property 22 | def height(self): 23 | return self.config.get("height", 400) 24 | 25 | # Process 26 | 27 | def process_snippet(self, snippet): 28 | if self.document.format == "html": 29 | if snippet.type.startswith("image") and snippet.lang == "yaml": 30 | context = yaml.safe_load(str(snippet.input).strip()) 31 | context.setdefault("width", self.width) 32 | context.setdefault("height", self.height) 33 | if snippet.type == "image": 34 | snippet.output = self.read_asset("base.html", **context) 35 | elif snippet.type == "image/instagram": 36 | snippet.output = self.read_asset("instagram.html", **context) 37 | 38 | def process_markup(self, markup): 39 | markup.add_style("style.css") 40 | -------------------------------------------------------------------------------- /pages/feature-reference/general.md: -------------------------------------------------------------------------------- 1 | # General 2 | 3 | General Livemark features are listed on this page. They are related to document processing and rendering. See other reference sections for more specific features like [Table](markdown.html#table) or [Search](navigation.html#search). 4 | 5 | ## Site 6 | 7 | To build a website (all the commands below are equal): 8 | 9 | ```bash 10 | $ livemark build 11 | $ livemark build index.md 12 | $ livemark build index.md --target index.html 13 | ``` 14 | 15 | Configuration, for example: 16 | 17 | ```yaml 18 | site: 19 | favicon: assets/favicon.ico 20 | styles: 21 | - style.css 22 | ``` 23 | 24 | The site will include following client-side software: 25 | 26 | - Bootstrap 5 27 | - Font Awesome 5 28 | - Lodash 4 29 | - jQuery 3 30 | 31 | ## HTML 32 | 33 | To build just a document without the site: 34 | 35 | ```python 36 | from livemark import Project 37 | 38 | project = Project('source.md', target='target.md', config={"site": False}) 39 | project.document.build() 40 | ``` 41 | 42 | ## Markdown 43 | 44 | To build a document as a Markdown: 45 | 46 | ```bash 47 | $ livemark build source.md --target target.md 48 | ``` 49 | 50 | Note that for the markdown rendering only a limited set of plugins are supported such as `script`. To use all of Livemark's features, it's better to render documents as HTML. 51 | -------------------------------------------------------------------------------- /pages/getting-started/starting-project.md: -------------------------------------------------------------------------------- 1 | # Starting a Project 2 | 3 | > Start quickly with [Livemark Project](https://github.com/frictionlessdata/livemark-project) Github Template 4 | 5 | Project is the most important concept in Livemark along side with Document. You create a project that contains one or more documents and optional [configuration](../configuration.html) file. 6 | 7 | ## Prepare 8 | 9 | Consider you're starting a new project called `my-project`: 10 | 11 | ```bash 12 | $ mkdir my-project 13 | $ cd my-project 14 | ``` 15 | 16 | Create a virtual environment (optional): 17 | 18 | ```bash 19 | $ python3 -m venv .python 20 | $ source .python/bin/activate 21 | ``` 22 | 23 | ## Bootstrap 24 | 25 | Livemark requires only a few steps from zero to a published project: 26 | 27 | First of all, create: 28 | - `index.md` 29 | - `livemark.yaml` 30 | 31 | Draft the main page (**this step is required**): 32 | 33 | > index.md 34 | 35 | ```md 36 | # My Project 37 | 38 | It will be great here 39 | ``` 40 | 41 | Fill in your configuration file: 42 | 43 | > livemark.yaml 44 | 45 | ```yaml 46 | brand: 47 | text: My Project 48 | site: 49 | favicon: assets/favicon.ico 50 | ``` 51 | 52 | ## Preview 53 | 54 | Now we're ready to start a livereload server: 55 | 56 | > http://localhost:7000 57 | 58 | ```bash 59 | $ livemark start 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /livemark/plugins/topics/style.css: -------------------------------------------------------------------------------- 1 | #livemark-topics { 2 | color: #888; 3 | } 4 | 5 | #livemark-topics .toc>.toc-list { 6 | overflow: hidden; 7 | position: relative; 8 | } 9 | 10 | #livemark-topics .toc>.toc-list li { 11 | list-style: none; 12 | } 13 | 14 | #livemark-topics .toc-list { 15 | padding-left: 0; 16 | margin: 0; 17 | } 18 | 19 | #livemark-topics a.toc-link { 20 | color: currentColor; 21 | } 22 | 23 | #livemark-topics a.toc-link:hover { 24 | color: #80b2e6; 25 | } 26 | 27 | #livemark-topics .is-active-link { 28 | font-weight: 700; 29 | } 30 | 31 | #livemark-topics ul.secondary { 32 | margin-left: 20px; 33 | display: none; 34 | } 35 | 36 | #livemark-topics li.is-active-li ul.secondary { 37 | display: block; 38 | } 39 | 40 | #livemark-topics li.group a.primary::after { 41 | content: "\f054"; 42 | font-size: 16px; 43 | font-family: "Font Awesome 5 Free"; 44 | font-weight: 900; 45 | position: absolute; 46 | top: 2px; 47 | right: 0px; 48 | } 49 | 50 | #livemark-topics li.group.is-active-li a.primary::after { 51 | content: "\f078"; 52 | } 53 | 54 | #livemark-topics a { 55 | display: inline-block; 56 | color: currentColor; 57 | position: relative; 58 | width: 100%; 59 | line-height: 100%; 60 | padding-top: 5px; 61 | padding-bottom: 5px; 62 | } 63 | 64 | #livemark-topics a:hover { 65 | color: #80b2e6; 66 | } 67 | -------------------------------------------------------------------------------- /livemark/plugins/links/plugin.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from ...plugin import Plugin 3 | 4 | 5 | class LinksPlugin(Plugin): 6 | identity = "links" 7 | priority = 10 8 | validity = { 9 | "type": "object", 10 | "properties": { 11 | "items": { 12 | "type": "array", 13 | "items": { 14 | "type": "object", 15 | "properties": { 16 | "name": {"type": "string"}, 17 | "path": {"type": "string"}, 18 | }, 19 | }, 20 | }, 21 | }, 22 | } 23 | 24 | # Context 25 | 26 | @property 27 | def items(self): 28 | github = self.document.get_plugin("github") 29 | items = deepcopy(self.config.get("items", [])) 30 | if github: 31 | if github.report_url: 32 | items.append({"name": "Report", "path": github.report_url}) 33 | if github.fork_url: 34 | items.append({"name": "Fork", "path": github.fork_url}) 35 | return items 36 | 37 | # Process 38 | 39 | def process_markup(self, markup): 40 | if self.document.path == "index": 41 | if self.items: 42 | markup.add_style("style.css") 43 | markup.add_markup("markup.html", target="#livemark-right") 44 | -------------------------------------------------------------------------------- /livemark/plugins/schema/plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | from ...plugin import Plugin 4 | from frictionless import Schema 5 | 6 | 7 | # NOTE: 8 | # Improve how we serialize/deseritalize the spec 9 | 10 | 11 | class SchemaPlugin(Plugin): 12 | identity = "schema" 13 | priority = 60 14 | 15 | # Process 16 | 17 | def process_document(self, document): 18 | self.__count = 0 19 | 20 | def process_snippet(self, snippet): 21 | if self.document.format == "html": 22 | if snippet.type == "schema" and snippet.lang in ["yaml", "json"]: 23 | if snippet.lang == "yaml": 24 | spec = yaml.safe_load(str(snippet.input).strip()) 25 | if snippet.lang == "json": 26 | spec = json.loads(str(snippet.input).strip()) 27 | spec = Schema(**spec).to_dict() 28 | self.__count += 1 29 | schema = {"spec": spec, "elem": f"livemark-schema-{self.__count}"} 30 | snippet.output = self.read_asset("markup.html", schema=schema) + "\n" 31 | 32 | def process_markup(self, markup): 33 | if self.__count: 34 | url = "https://unpkg.com/frictionless-components@1.0.1" 35 | markup.add_style(f"{url}/dist/frictionless-components.min.css") 36 | markup.add_script(f"{url}/dist/frictionless-components.min.js") 37 | -------------------------------------------------------------------------------- /pages/getting-started/building-website.md: -------------------------------------------------------------------------------- 1 | # Building a Website 2 | 3 | Livemark is a very simple static site generator design-wise. It takes your Markdown documents as input and outputs HTML documents. The result can be deployed as it is to any static site hosting. 4 | 5 | ## Build 6 | 7 | You can then use the command-line interface to build the output HTML file: 8 | 9 | ```bash 10 | # Build a single document (index.md by default) 11 | $ livemark build 12 | ``` 13 | 14 | Or start a livereload server to automatically reload the output page as you modify the input Markdown document: 15 | 16 | ```bash 17 | # Start a livereload server 18 | $ livemark start 19 | ``` 20 | 21 | Both commands will create an `index.md` file if it's not present in the same folder the command is being run on. If that's not the case you can pass the path to the input Markdown file as the first parameter: 22 | 23 | ```bash 24 | $ livemark build path/to/your/file.md 25 | $ livemark start path/to/your/file.md 26 | ``` 27 | 28 | ## Publish 29 | 30 | > https://pages.github.com/ 31 | 32 | Livemark generates a static HTML document so you can publish it using any static page hosting. A common option for hosting is to use Github Pages - go to "Settings->Pages" in your repository and choose your main branch in the source menu: 33 | 34 | ```yaml image 35 | path: ../../assets/deploy.png 36 | width: 75% 37 | height: unset 38 | class: border 39 | ``` 40 | -------------------------------------------------------------------------------- /livemark/plugins/source/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | // Add buttons 3 | $("h2").append('Source'); 4 | 5 | // Enable buttons 6 | $(".livemark-source-button").click(async (ev) => { 7 | ev.preventDefault(); 8 | 9 | // Close open 10 | if ($(".livemark-source-section").length) { 11 | $(".livemark-source-section").remove(); 12 | return; 13 | } 14 | 15 | // Load content 16 | let source = location.href.replace(".html", ".md"); 17 | if (!source.endsWith(".md")) source = `${source}index.md`; 18 | const heading = $(ev.target).parent().contents().get(0).nodeValue; 19 | response = await fetch(source); 20 | content = await response.text(); 21 | 22 | // Extract section 23 | let isCapture; 24 | const lines = []; 25 | for (const line of content.split(/\r?\n/)) { 26 | if (line.startsWith("##")) { 27 | isCapture = line.startsWith(`## ${heading}`) ? true : false; 28 | continue; 29 | } 30 | if (isCapture) { 31 | lines.push(line); 32 | } 33 | } 34 | const section = _.escape(lines.join("\n").trim()); 35 | 36 | // Show section 37 | $(ev.target) 38 | .parent() 39 | .after( 40 | `
${section}
` 41 | ); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /pages/feature-reference/others.md: -------------------------------------------------------------------------------- 1 | # Others 2 | 3 | ## Cleanup 4 | 5 | This feature provides an ability to specify a list of Bash command to be run after the document building: 6 | 7 | > livemark.yaml 8 | 9 | ```yaml 10 | cleanup: 11 | - rm table.csv 12 | ``` 13 | 14 | ## Counter 15 | 16 | This feature currently supports a Google Analytics counter that can be added to all the project pages: 17 | 18 | > livemark.yaml 19 | 20 | ```yaml 21 | counter: 22 | type: google 23 | code: G- 24 | ``` 25 | 26 | ## Github 27 | 28 | Some plugins rely on Github repository information that is provided by this feature. By default, it will be inferred automatically from you local `.git` directory. It's also possible to configure it manually: 29 | 30 | > livemark.yaml 31 | 32 | ```yaml 33 | github: 34 | user: frictionlessdata 35 | repo: livemark 36 | ``` 37 | 38 | ## Prepare 39 | 40 | This feature provides an ability to specify a list of Bash command to be run before the document building: 41 | 42 | > livemark.yaml 43 | 44 | ```yaml 45 | prepare: 46 | - cp data/table.csv table.csv 47 | ``` 48 | 49 | ## Redirect 50 | 51 | Using Github Pages for hosting, it's possible to setup a redirect table: 52 | 53 | > livemark.yaml 54 | 55 | ```yaml 56 | redirect: 57 | items: 58 | - prev: getting-started 59 | next: pages/installation 60 | ``` 61 | 62 | Under the hood, Livemark will create a `404.html` file and use a client-side redirect. 63 | -------------------------------------------------------------------------------- /livemark/plugins/display/style.css: -------------------------------------------------------------------------------- 1 | #livemark-display { 2 | display: flex; 3 | position: fixed; 4 | visibility: hidden; 5 | justify-content: space-between; 6 | font-size: 16px; 7 | color: #888; 8 | width: 240px; 9 | bottom: 20px; 10 | right: 30px; 11 | } 12 | 13 | @media only screen and (min-width: 992px) { 14 | #livemark-display { 15 | visibility: visible; 16 | } 17 | } 18 | 19 | #livemark-display .control { 20 | cursor: pointer; 21 | background-color:#fff; 22 | box-shadow: 0px 7px 10px #eee; 23 | border-radius: 50%; 24 | border: solid 1px #ddd; 25 | z-index: 100; 26 | } 27 | 28 | #livemark-display .control .fa { 29 | display: inline-block !important; 30 | opacity: 1 !important; 31 | padding: 15px; 32 | } 33 | 34 | .with-readability { 35 | font-size: 20px; 36 | } 37 | 38 | .with-readability #livemark-main { 39 | color: #000; 40 | } 41 | 42 | .with-readability #livemark-left > *, 43 | .with-readability #livemark-right > * { 44 | color: #444; 45 | } 46 | 47 | .with-readability #livemark-left, 48 | .with-readability #livemark-right { 49 | scrollbar-width: unset; /* Firefox */ 50 | -ms-overflow-style: unset; /* Internet Explorer 10+ */ 51 | } 52 | 53 | /* NOTE: */ 54 | /* temporarily implemented in HtmlPlugin */ 55 | /* .with-readability #livemark-left::-webkit-scrollbar, */ 56 | /* .with-readability #livemark-right::-webkit-scrollbar { */ 57 | /* display: unset !important; [> Chrome; Safari <] */ 58 | /* } */ 59 | -------------------------------------------------------------------------------- /livemark/plugins/report/plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | from ...plugin import Plugin 4 | 5 | 6 | # NOTE: 7 | # Improve how we serialize/deseritalize the spec 8 | 9 | 10 | class ReportPlugin(Plugin): 11 | identity = "report" 12 | priority = 60 13 | 14 | # Process 15 | 16 | def process_document(self, document): 17 | self.__count = 0 18 | 19 | def process_snippet(self, snippet): 20 | if self.document.format == "html": 21 | if snippet.type == "report" and snippet.lang in ["yaml", "json"]: 22 | spec = None 23 | if snippet.lang == "yaml": 24 | spec = yaml.safe_load(str(snippet.input).strip()) 25 | if snippet.lang == "json": 26 | spec = json.loads(str(snippet.input).strip()) 27 | if spec and spec.get("descriptor"): 28 | self.__count += 1 29 | report = { 30 | "spec": spec["descriptor"], 31 | "elem": f"livemark-report-{self.__count}", 32 | } 33 | snippet.output = self.read_asset("markup.html", report=report) + "\n" 34 | 35 | def process_markup(self, markup): 36 | if self.__count: 37 | url = "https://unpkg.com/frictionless-components@1.0.1" 38 | markup.add_style(f"{url}/dist/frictionless-components.min.css") 39 | markup.add_script(f"{url}/dist/frictionless-components.min.js") 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask instance folder 57 | instance/ 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | # IPython Notebook 69 | .ipynb_checkpoints 70 | 71 | # pyenv 72 | .python-version 73 | 74 | # dotenv 75 | .env 76 | 77 | # Spyder project settings 78 | .spyderproject 79 | 80 | # Livemark 81 | blog/**/*.html 82 | pages/**/*.html 83 | index.html 84 | 404.html 85 | 86 | # Extras 87 | .projectile 88 | tabulator 89 | jsontableschema 90 | datapackage 91 | .mypy_cache 92 | !docs/build 93 | .google.json 94 | tmp 95 | -------------------------------------------------------------------------------- /livemark/plugins/redirect/plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ...plugin import Plugin 3 | from ...document import Document 4 | from ... import helpers 5 | 6 | 7 | class RedirectPlugin(Plugin): 8 | priority = 1000 9 | identity = "redirect" 10 | validity = { 11 | "type": "object", 12 | "properties": { 13 | "items": { 14 | "type": "array", 15 | "items": { 16 | "type": "object", 17 | "required": ["prev", "next"], 18 | "properties": { 19 | "prev": {"type": "string"}, 20 | "next": {"type": "string"}, 21 | }, 22 | }, 23 | }, 24 | }, 25 | } 26 | 27 | # Context 28 | 29 | @property 30 | def items(self): 31 | return self.config.get("items", []) 32 | 33 | # Process 34 | 35 | @staticmethod 36 | def process_project(project): 37 | items = project.config.get("redirect", {}).get("items", []) 38 | if items: 39 | missing_default = os.path.join(os.path.dirname(__file__), "missing.md") 40 | missing_source = "404.md" 41 | if not os.path.isfile(missing_source): 42 | helpers.copy_file(missing_default, missing_source) 43 | project.documents.append(Document(missing_source, project=project)) 44 | 45 | def process_markup(self, markup): 46 | if self.document.path == "404": 47 | markup.add_markup("redirect.html", target="head") 48 | -------------------------------------------------------------------------------- /livemark/plugins/mobile/style.css: -------------------------------------------------------------------------------- 1 | #livemark-mobile { 2 | position: absolute; 3 | visibility: hidden; 4 | z-index: 10000; 5 | /* NOTE: We can't use "right" because of Mobile Chrome and #34 */ 6 | left: calc(100vw - 60px); 7 | top: 24px; 8 | } 9 | 10 | #livemark-mobile .stack { 11 | margin-top: 15px; 12 | display: block; 13 | cursor: pointer; 14 | } 15 | 16 | #livemark-mobile .bar { 17 | display: block; 18 | width: 25px; 19 | height: 3px; 20 | margin: 5px auto; 21 | -webkit-transition: all 0.3s ease-in-out; 22 | transition: all 0.3s ease-in-out; 23 | background-color: #aaa; 24 | } 25 | 26 | @media only screen and (max-width: 768px) { 27 | #livemark-mobile { 28 | visibility: visible; 29 | } 30 | 31 | #livemark-mobile.active { 32 | position: fixed; 33 | } 34 | 35 | #livemark-mobile.active .bar:nth-child(2) { 36 | opacity: 0; 37 | } 38 | 39 | #livemark-mobile.active .bar:nth-child(1) { 40 | transform: translateY(8px) rotate(45deg); 41 | } 42 | 43 | #livemark-mobile.active .bar:nth-child(3) { 44 | transform: translateY(-8px) rotate(-45deg); 45 | } 46 | 47 | #livemark-left { 48 | position: fixed; 49 | top: 0; 50 | left: -100vw; 51 | padding-top: 35px; 52 | background-color: #fff; 53 | width: 100vw; 54 | border-radius: 10px; 55 | text-align: center; 56 | transition: 0.3s; 57 | box-shadow: 0 10px 27px rgba(0, 0, 0, 0.05); 58 | visibility: visible; 59 | z-index: 1000; 60 | } 61 | 62 | #livemark-left.active { 63 | left: 0; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /livemark/plugins/chart/plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | from ...plugin import Plugin 4 | 5 | 6 | class ChartPlugin(Plugin): 7 | identity = "chart" 8 | priority = 60 9 | 10 | # Process 11 | 12 | def process_document(self, document): 13 | self.__count = 0 14 | 15 | def process_snippet(self, snippet): 16 | if self.document.format == "html": 17 | if snippet.type == "chart" and snippet.lang in ["yaml", "json"]: 18 | if snippet.lang == "yaml": 19 | spec = yaml.safe_load(str(snippet.input).strip()) 20 | if snippet.lang == "json": 21 | spec = json.loads(str(snippet.input).strip()) 22 | spec = json.dumps(spec, ensure_ascii=False) 23 | spec = spec.replace("'", "\\'") 24 | self.__count += 1 25 | card = snippet.props.get("card") 26 | elem = f"livemark-chart-{self.__count}" 27 | if card: 28 | elem += "-card" 29 | chart = {"spec": spec, "elem": elem} 30 | snippet.output = ( 31 | self.read_asset("markup.html", card=card, chart=chart) + "\n" 32 | ) 33 | 34 | def process_markup(self, markup): 35 | if self.__count: 36 | url = "https://unpkg.com" 37 | markup.add_script(f"{url}/vega@5.20.2/build/vega.min.js") 38 | markup.add_script(f"{url}/vega-lite@5.1.0/build/vega-lite.min.js") 39 | markup.add_script(f"{url}/vega-embed@6.18.2/build/vega-embed.min.js") 40 | -------------------------------------------------------------------------------- /livemark/server.py: -------------------------------------------------------------------------------- 1 | import livereload 2 | from . import settings 3 | 4 | 5 | # NOTE: 6 | # Consider implementing `server.stop` although it's not supported in livereload 7 | 8 | 9 | class Server: 10 | """Livemark server 11 | 12 | Parameters: 13 | proejct (Project): a project to server 14 | 15 | """ 16 | 17 | def __init__(self, project): 18 | self.__project = project 19 | self.__server = livereload.Server() 20 | 21 | @property 22 | def project(self): 23 | """Server's project 24 | 25 | Return: 26 | Project: project 27 | """ 28 | return self.__project 29 | 30 | # Start 31 | 32 | def start( 33 | self, 34 | *, 35 | host=settings.DEFAULT_HOST, 36 | port=settings.DEFAULT_PORT, 37 | file=settings.DEFAULT_FILE, 38 | ): 39 | """Start the server 40 | 41 | Parameters: 42 | host (str): HTTP host 43 | port (int): HTTP port 44 | file (str): index file path 45 | """ 46 | 47 | # Build documents 48 | self.project.build() 49 | for source in self.project.building_sources: 50 | self.__server.watch(source, self.project.build, delay=1) 51 | for document in self.project.building_documents: 52 | self.__server.watch(document.source, document.build, delay=1) 53 | 54 | # Run server 55 | self.__server.serve( 56 | host=host, 57 | port=port, 58 | root=".", 59 | open_url_delay=1, 60 | default_filename=file, 61 | ) 62 | -------------------------------------------------------------------------------- /livemark/plugins/markdown/renderer.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from marko import md_renderer 3 | from marko.inline import RawText 4 | from marko.block import FencedCode 5 | from ...snippet import Snippet 6 | 7 | 8 | class MarkdownRenderer(md_renderer.MarkdownRenderer): 9 | # Render 10 | 11 | def render_quote(self, element): 12 | return super().render_quote(element).rstrip() + "\n" 13 | 14 | def render_fenced_code(self, element): 15 | input = self.render_children(element).strip() 16 | header = [element.lang] + element.extra.split() 17 | snippet = Snippet(input, header=header) 18 | snippet.process(self.document) 19 | if snippet.output is not None: 20 | # Locate target 21 | target = None 22 | index = self.root_node.children.index(element) 23 | if len(self.root_node.children) > index + 1: 24 | item = self.root_node.children[index + 1] 25 | if isinstance(item, FencedCode): 26 | target = item 27 | 28 | # Update target 29 | if snippet.output: 30 | if not target: 31 | target = copy(element) 32 | target.lang = "" 33 | target.extra = "" 34 | self.root_node.children.insert(index + 1, target) 35 | target.children = [RawText(snippet.output)] 36 | 37 | # Delete target 38 | if not snippet.output: 39 | if target: 40 | del self.root_node.children[index + 1] 41 | 42 | return super().render_fenced_code(element) 43 | -------------------------------------------------------------------------------- /livemark/plugins/github/plugin.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from giturlparse import parse 3 | from ...plugin import Plugin 4 | 5 | 6 | class GithubPlugin(Plugin): 7 | identity = "github" 8 | validity = { 9 | "type": "object", 10 | "required": ["user", "repo"], 11 | "properties": { 12 | "user": {"type": "string"}, 13 | "repo": {"type": "string"}, 14 | }, 15 | } 16 | 17 | def __init__(self, document): 18 | super().__init__(document) 19 | 20 | # Infer data 21 | try: 22 | git = importlib.import_module("git") 23 | repo = git.Repo() 24 | pack = parse(repo.remote().url) 25 | self.__data = {"user": pack.owner, "repo": pack.repo} 26 | except Exception: 27 | self.__data = {} 28 | 29 | # Context 30 | 31 | @property 32 | def user(self): 33 | return self.config.get("user", self.__data.get("user")) 34 | 35 | @property 36 | def repo(self): 37 | return self.config.get("repo", self.__data.get("repo")) 38 | 39 | @property 40 | def base_url(self): 41 | if self.user and self.repo: 42 | return f"https://github.com/{self.user}/{self.repo}" 43 | 44 | @property 45 | def report_url(self): 46 | if self.base_url: 47 | return f"{self.base_url}/issues" 48 | 49 | @property 50 | def fork_url(self): 51 | if self.base_url: 52 | return f"{self.base_url}/fork" 53 | 54 | @property 55 | def edit_url(self): 56 | if self.base_url: 57 | return f"{self.base_url}/edit/main/{self.document.source}" 58 | -------------------------------------------------------------------------------- /pages/getting-started/using-markdown.md: -------------------------------------------------------------------------------- 1 | # Using Markdown 2 | 3 | Markdown is a lightweight markup language for creating formatted text using a plain-text editor. You use Markdown to write documents in your Livemark projects. With Livemark you can use all the base Markdown features along side with ones added by Github and Livemark itself. 4 | 5 | ## Base Markdown 6 | 7 | Base Markdown functionality includes: 8 | 9 | - Headings 10 | - Text styles 11 | - Syntax Highlighting 12 | - Horizontal Rule 13 | - Alignments 14 | - Tables 15 | - Links 16 | - Images 17 | - Lists 18 | 19 | There are plenty of great Markdown guides, to name a few: 20 | 21 | - [Markdown Guide](https://guides.github.com/features/mastering-markdown/) 22 | - [Markdown Cheat Sheet](https://towardsdatascience.com/the-ultimate-markdown-cheat-sheet-3d3976b31a0) 23 | 24 | We encourage you to learn Markdown first to use it in Livemark. 25 | 26 | ## Github Extensions 27 | 28 | Livemark supports Markdown extensions used by Github. It includes: 29 | 30 | - URL autolinking 31 | - Strikethrough text 32 | - Fenced code blocks (and syntax highlighting) 33 | - Task Lists 34 | 35 | Learn more about Github Flavoured Markdown (GFM) here: 36 | 37 | - [Mastering Markdown](https://guides.github.com/features/mastering-markdown/) 38 | 39 | ## Livemark Extensions 40 | 41 | On top of all the mentioned features, Livemark makes markdown even better! It supports (mostly interactive): 42 | 43 | - Logic 44 | - Tables 45 | - Charts 46 | - Scripts 47 | - Markup 48 | - Frictionless Data processing 49 | - more 50 | 51 | Read Livemark's documentation to learn more: 52 | 53 | - [Feature Reference](../feature-reference.html) 54 | -------------------------------------------------------------------------------- /data/cars.csv: -------------------------------------------------------------------------------- 1 | brand,model,price,kmpl,bhp,type 2 | Volkswagen,Vento,785,16.1,104,Sedan 3 | Hyundai,Verna,774,17.4,106,Sedan 4 | Skoda,Rapid,756,15,104,Sedan 5 | Suzuki,Ciaz,725,20.7,91,Sedan 6 | Renault,Scala,724,16.9,98,Sedan 7 | Suzuki,SX4,715,16.5,103,Sedan 8 | Fiat,Linea,700,15.7,112,Sedan 9 | Nissan,Sunny,699,16.9,98,Sedan 10 | Fiat,Linea Classic,612,14.9,89,Sedan 11 | Toyota,Etios,603,16.8,89,Sedan 12 | San,Storm,595,16,59,Sedan 13 | Chevrolet,Sail,551,18.2,82,Sedan 14 | Volkswagen,Polo,535,16.5,74,Hatchback 15 | Hyundai,i20,523,18.6,82,Hatchback 16 | Honda,Amaze,519,18,87,Sedan 17 | Suzuki,Swift DZire,508,19.1,86,Sedan 18 | Ford,Classic,506,14.1,100,Sedan 19 | Skoda,Fabia,503,16.4,75,Hatchback 20 | Toyota,Etios Liva,500,17.7,79,Hatchback 21 | Fiat,Punto Evo,499,15.8,67,Hatchback 22 | Tata,Indigo,499,14,65,Sedan 23 | Hyundai,Xcent,496,19.1,82,Sedan 24 | Tata,Zest,481,17.6,89,Sedan 25 | Chevrolet,Sail Hatchback,468,18.2,82,Hatchback 26 | Suzuki,Swift,462,20.4,83,Hatchback 27 | Renault,Pulse,446,18,74,Hatchback 28 | Suzuki,Ritz,442,18.5,86,Hatchback 29 | Chevrolet,Beat,421,18.6,79,Hatchback 30 | Honda,Brio,421,19.4,87,Hatchback 31 | Hyundai,i10,418,19.8,68,Hatchback 32 | Ford,Figo,414,15.3,70,Hatchback 33 | Nissan,Micra,413,19.5,67,Hatchback 34 | Suzuki,Celerio,392,23.1,67,Hatchback 35 | Suzuki,Wagon-R,363,20.5,67,Hatchback 36 | Volkswagen,Up,360,21,74,Hatchback 37 | Chevrolet,Spark,345,16.2,62,Hatchback 38 | Suzuki,Estilo,338,19,67,Hatchback 39 | Suzuki,Alto,315,24.1,67,Hatchback 40 | Nissan,Datsun GO,312,20.6,67,Hatchback 41 | Hyundai,EON,302,21.1,55,Hatchback 42 | Suzuki,Alto 800,248,22.7,47,Hatchback 43 | Tata,Nano,199,23.9,38,Hatchback -------------------------------------------------------------------------------- /blog/2021-09-01-meet-livemark.md: -------------------------------------------------------------------------------- 1 | --- 2 | blog: 3 | author: Sara Petti 4 | image: ../assets/example.png 5 | --- 6 | 7 | # Meet Livemark 8 | 9 | We are very excited to announce that a new tool has been added to the Frictionless Data toolkit: Livemark. What is that? Livemark is a great tool that allows you to publish data articles very easily, giving you the possibility to see your data live on a working website in a blink of an eye. 10 | 11 | ## How does it work? 12 | 13 | Livemark is a Python library generating a static page that extends Markdown with interactive charts, tables, scripts, and much much more. You can use the Frictionless framework as a `frictionless` variable to work with your tabular data in Livemark. 14 | 15 | Livemark offers a series of useful features, like automatically generating a table of contents and providing a scroll-to-top button when you scroll down your document. You can also customise the layout of your newly created webpage. 16 | 17 | ## How can you get started? 18 | Livemark is very easy to use. We invite you watch this great demo by developer Evgeny Karev: 19 | 20 | ```yaml video/youtube 21 | code: NMg-eCbO6L0 22 | ``` 23 | 24 | You can also have a look at the [documentation on GitHub](https://frictionlessdata.github.io/livemark/). 25 | 26 | ## What do you think? 27 | If you create a site using Livemark, please let us know! Frictionless Data is an open source project, therefore we encourage you to give us feedback. Let us know your thoughts, suggestions, or issues by joining us in our community chat on [Discord]( https://discord.com/invite/Sewv6av) or by opening an issue in the [GitHub repo](https://github.com/frictionlessdata/livemark). 28 | -------------------------------------------------------------------------------- /livemark/plugins/search/style.css: -------------------------------------------------------------------------------- 1 | #livemark-search { 2 | position: fixed; 3 | left: 30px; 4 | width: 240px; 5 | bottom: 20px; 6 | z-index:100; 7 | visibility: hidden; 8 | } 9 | 10 | @media only screen and (min-width: 992px) { 11 | #livemark-search { 12 | visibility: visible; 13 | } 14 | } 15 | 16 | #livemark-search-input { 17 | width: 100%; 18 | outline: none; 19 | font-size: 20px; 20 | padding: 7px 10px; 21 | border-radius: 20px; 22 | border: solid 1px #ddd; 23 | box-shadow: 0px 7px 10px #eee; 24 | color: #888; 25 | } 26 | 27 | #livemark-search-input::placeholder { 28 | color: #888; 29 | } 30 | 31 | #livemark-search-input::-webkit-search-decoration, 32 | #livemark-search-input::-webkit-search-cancel-button, 33 | #livemark-search-input::-webkit-search-results-button, 34 | #livemark-search-input::-webkit-search-results-decoration { 35 | -webkit-appearance:none; 36 | } 37 | 38 | #livemark-search-output { 39 | visibility: hidden; 40 | width: 100%; 41 | font-size: 16px; 42 | padding: 10px 10px; 43 | border-radius: 20px; 44 | border: solid 1px #ddd; 45 | box-shadow: 0px 7px 10px #eee; 46 | background: white; 47 | margin-bottom: 10px; 48 | } 49 | 50 | #livemark-search-output ul { 51 | margin: 0; 52 | padding: 0; 53 | list-style-type: none; 54 | } 55 | 56 | #livemark-search-output li.active { 57 | font-size: 20px; 58 | font-weight: bold; 59 | } 60 | 61 | #livemark-search-output a { 62 | color: #5CC820; 63 | text-decoration: underline; 64 | } 65 | 66 | .livemark-search-found { 67 | background-color: #5CC820; 68 | color: white; 69 | font-weight: bold; 70 | padding: 5px; 71 | border-radius: 5px; 72 | } 73 | -------------------------------------------------------------------------------- /livemark/program/start.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import typer 4 | from ..project import Project 5 | from ..server import Server 6 | from .main import program 7 | from .. import settings 8 | from .. import helpers 9 | from . import common 10 | 11 | 12 | # NOTE: 13 | # We need to improve the default template by making it faster and more informative 14 | 15 | 16 | @program.command(name="start") 17 | def program_start( 18 | source: str = common.source, 19 | target: str = common.target, 20 | format: str = common.format, 21 | config: str = common.config, 22 | host: str = common.host, 23 | port: str = common.port, 24 | ): 25 | """Start a Livemark server.""" 26 | 27 | try: 28 | # Handle config 29 | if not config: 30 | if os.path.exists(settings.DEFAULT_CONFIG): 31 | config = settings.DEFAULT_CONFIG 32 | 33 | # Handle source 34 | if not source and not config: 35 | if os.path.exists(settings.DEFAULT_SOURCE): 36 | source = settings.DEFAULT_SOURCE 37 | 38 | # Bootstrap project 39 | if not source and not config: 40 | source = settings.DEFAULT_SOURCE 41 | helpers.copy_file(settings.TEMPLATE, source) 42 | 43 | # Create project 44 | project = Project( 45 | source, 46 | target=target, 47 | format=format, 48 | config=config, 49 | ) 50 | 51 | # Live mode 52 | server = Server(project) 53 | server.start(host=host, port=port) 54 | 55 | except Exception as exception: 56 | typer.secho(str(exception), err=True, fg=typer.colors.RED, bold=True) 57 | sys.exit(1) 58 | -------------------------------------------------------------------------------- /livemark/plugins/markup/plugin.py: -------------------------------------------------------------------------------- 1 | import marko 2 | from marko.ext.gfm import GFM 3 | from ...plugin import Plugin 4 | 5 | 6 | # NOTE: 7 | # We might consider rebase markdown blocks rendering on using Document 8 | # This change will remove the direct HtmlException and GFM dependencies 9 | 10 | 11 | class MarkupPlugin(Plugin): 12 | identity = "markup" 13 | priority = 60 14 | 15 | # Process 16 | 17 | def process_document(self, document): 18 | self.__jsx_count = 0 19 | 20 | def process_snippet(self, snippet): 21 | if self.document.format == "html": 22 | if snippet.type == "markup" and snippet.lang in ["markdown", "html", "jsx"]: 23 | # Markdown 24 | if snippet.lang == "markdown": 25 | markdown = marko.Markdown() 26 | markdown.use(GFM) 27 | snippet.output = markdown.convert(snippet.input) 28 | 29 | # Html 30 | elif snippet.lang == "html": 31 | snippet.output = snippet.input 32 | 33 | # Jsx 34 | elif snippet.lang == "jsx": 35 | self.__jsx_count += 1 36 | context = {} 37 | context["content"] = snippet.input 38 | context["element"] = self.__jsx_count 39 | snippet.output = self.read_asset("markup.html", **context) + "\n" 40 | 41 | def process_markup(self, markup): 42 | if self.__jsx_count: 43 | url = "https://unpkg.com" 44 | markup.add_script(f"{url}/react@17.0.2/umd/react.production.min.js") 45 | markup.add_script(f"{url}/react-dom@17.0.2/umd/react-dom.production.min.js") 46 | markup.add_script(f"{url}/babel-standalone@6.26.0/babel.min.js") 47 | -------------------------------------------------------------------------------- /livemark/plugins/topics/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | // Start tocbot 3 | tocbot.init({ 4 | // Where to render the table of contents. 5 | tocSelector: ".toc", 6 | // Where to grab the headings to build the table of contents. 7 | contentSelector: "#livemark-main", 8 | // Which headings to grab inside of the contentSelector element. 9 | headingSelector: "{{ plugin.selector }}", 10 | // For headings inside relative or absolute positioned containers within content. 11 | hasInnerContainers: true, 12 | // Called each time a heading is parsed. Expects a string in return. 13 | headingLabelCallback: (label) => { 14 | label = label.replace(/(^#|#$)/g, "").trim(); 15 | // label = label.replace(/\(.*?\)$/g, ""); 16 | return label; 17 | }, 18 | // Disable generating ordered lists (ol) 19 | orderedList: false, 20 | // Fix active link class 21 | onClick: syncList, 22 | scrollEndCallback: syncList, 23 | }); 24 | 25 | // Style list 26 | $("#livemark-topics .toc > ul").addClass("primary"); 27 | $("#livemark-topics .toc > ul > li").addClass("primary"); 28 | $("#livemark-topics .toc > ul > li > a").addClass("primary"); 29 | $("#livemark-topics .toc ul.is-collapsible").addClass("secondary"); 30 | $("#livemark-topics .toc ul.is-collapsible li").addClass("secondary"); 31 | $("#livemark-topics .toc ul.is-collapsible li > a").addClass("secondary"); 32 | for (const element of $("#livemark-topics .primary")) { 33 | if ($(element).find(".secondary").length) { 34 | $(element).addClass("group"); 35 | } 36 | } 37 | 38 | // Sync list 39 | function syncList() { 40 | for (const element of $("#livemark-topics li.primary")) { 41 | if ($(element).find(".is-active-li").length) { 42 | $(element).addClass("is-active-li"); 43 | } 44 | } 45 | } 46 | syncList(); 47 | }); 48 | -------------------------------------------------------------------------------- /livemark/plugins/signs/plugin.py: -------------------------------------------------------------------------------- 1 | from ...plugin import Plugin 2 | from ... import helpers 3 | 4 | 5 | # NOTE: 6 | # review why we check that self.document.project exists in items 7 | 8 | 9 | class SignsPlugin(Plugin): 10 | identity = "signs" 11 | priority = 40 12 | 13 | # Context 14 | 15 | @property 16 | def current(self): 17 | return self.document.path 18 | 19 | @property 20 | def items(self): 21 | if self.document.project: 22 | documents = self.document.project.documents 23 | if documents: 24 | prev = None 25 | next = None 26 | current_number = None 27 | for number, document in enumerate(documents, start=1): 28 | if document.path == self.document.path: 29 | current_number = number 30 | if current_number: 31 | if current_number > 1: 32 | document = documents[current_number - 2] 33 | # TODO: have a concept of public/hidden page? 34 | if document.path != "404": 35 | path = helpers.get_url_relpath(document.path, self.current) 36 | prev = {"name": document.name, "path": path} 37 | if current_number < len(documents): 38 | document = documents[current_number] 39 | # TODO: have a concept of public/hidden page? 40 | if document.path != "404": 41 | path = helpers.get_url_relpath(document.path, self.current) 42 | next = {"name": document.name, "path": path} 43 | return {"prev": prev, "next": next} 44 | 45 | # Process 46 | 47 | def process_markup(self, markup): 48 | if self.items: 49 | markup.add_style("style.css") 50 | markup.add_markup("markup.html", target="#livemark-main") 51 | -------------------------------------------------------------------------------- /pages/guides-and-tutorials/software-education.md: -------------------------------------------------------------------------------- 1 | # Software Education 2 | 3 | Livemark is perfectly suited for writing education materials as it uses code execution model in markdown documents. This means that as an author you only need to write code snippet inputs while outputs will be automatically inserted into your articles. It solves a range of problems with testing and having your code examples up-to-date. 4 | 5 | ## Example 6 | 7 | Note that this project is under development: 8 | 9 | > https://frictionlessdata.github.io/learning-python/ 10 | 11 | ```yaml image 12 | path: ../../assets/education.png 13 | width: 100% 14 | height: unset 15 | class: border 16 | ``` 17 | 18 | ## Quick Start 19 | 20 | > Start from [Github Template](https://github.com/frictionlessdata/livemark-project) if you want the quickest setup 21 | 22 | Livemark requires only a few steps from zero to a published project: 23 | 24 | First of all, create: 25 | - `livemark.yaml` 26 | - `index.md` 27 | - `pages/data.md` (for example) 28 | 29 | Fill in your configuration file: 30 | 31 | > livemark.yaml 32 | 33 | ```yaml 34 | brand: 35 | text: My Project 36 | about: 37 | text: My project is for data journalism 38 | site: 39 | favicon: assets/favicon.ico 40 | github: 41 | user: 42 | repo: 43 | topics: 44 | selector: h2 45 | links: 46 | items: 47 | - name: About Me 48 | path: https://personal.site 49 | pages: 50 | items: 51 | - name: Introduction 52 | path: index 53 | - path: pages/data 54 | ``` 55 | 56 | Run a livereload server locally: 57 | 58 | ```bash 59 | $ livemark start 60 | ``` 61 | 62 | When you are ready to publish your work, commit the changes and push it to Github. The only missing part now is enabling Github Pages: 63 | 64 | > https://guides.github.com/features/pages/ 65 | 66 | ```yaml image 67 | path: ../../assets/deploy.png 68 | width: 75% 69 | height: unset 70 | class: border 71 | ``` 72 | 73 | After this step your documentation portal will be up and running. 74 | -------------------------------------------------------------------------------- /livemark/program/build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import typer 4 | from ..server import Server 5 | from ..project import Project 6 | from .main import program 7 | from .. import settings 8 | from .. import errors 9 | from . import common 10 | 11 | 12 | # NOTE: 13 | # Live mode always opens index file even though another source is provided 14 | 15 | 16 | @program.command(name="build") 17 | def program_build( 18 | source: str = common.source, 19 | target: str = common.target, 20 | format: str = common.format, 21 | config: str = common.config, 22 | print: bool = common.print, 23 | diff: bool = common.diff, 24 | live: bool = common.live, 25 | host: str = common.host, 26 | port: str = common.port, 27 | ): 28 | """Build Markdown file into HTML by default.""" 29 | 30 | try: 31 | # Handle config 32 | if not config: 33 | if os.path.exists(settings.DEFAULT_CONFIG): 34 | config = settings.DEFAULT_CONFIG 35 | 36 | # Handle source 37 | if not source and not config: 38 | if os.path.exists(settings.DEFAULT_SOURCE): 39 | source = settings.DEFAULT_SOURCE 40 | 41 | # Validate project 42 | if not source and not config: 43 | message = 'Project without "source" requires "config"' 44 | raise errors.Error(message) 45 | 46 | # Create project 47 | project = Project( 48 | source, 49 | target=target, 50 | format=format, 51 | config=config, 52 | ) 53 | 54 | # Normal mode 55 | if not live: 56 | output = project.build(diff=diff, print=print) 57 | if output and diff: 58 | sys.exit(1) 59 | sys.exit(0) 60 | 61 | # Live mode 62 | server = Server(project) 63 | server.start(host=host, port=port) 64 | 65 | except Exception as exception: 66 | typer.secho(str(exception), err=True, fg=typer.colors.RED, bold=True) 67 | sys.exit(1) 68 | -------------------------------------------------------------------------------- /data/cars.report.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.11.0", 3 | "time": 0.04, 4 | "errors": [], 5 | "tasks": [ 6 | { 7 | "resource": { 8 | "path": "data/cars.csv", 9 | "name": "cars", 10 | "profile": "tabular-data-resource", 11 | "scheme": "file", 12 | "format": "csv", 13 | "hashing": "md5", 14 | "encoding": "utf-8", 15 | "schema": { 16 | "fields": [ 17 | { 18 | "name": "brand", 19 | "type": "string" 20 | }, 21 | { 22 | "name": "model", 23 | "type": "string" 24 | }, 25 | { 26 | "name": "price", 27 | "type": "integer" 28 | }, 29 | { 30 | "name": "kmpl", 31 | "type": "number" 32 | }, 33 | { 34 | "name": "bhp", 35 | "type": "integer" 36 | }, 37 | { 38 | "name": "type", 39 | "type": "string" 40 | } 41 | ] 42 | }, 43 | "stats": { 44 | "hash": "6a601532555ece341408e562511d3561", 45 | "bytes": 1497, 46 | "fields": 6, 47 | "rows": 42 48 | } 49 | }, 50 | "time": 0.04, 51 | "scope": [ 52 | "hash-count-error", 53 | "byte-count-error", 54 | "field-count-error", 55 | "row-count-error", 56 | "blank-header", 57 | "extra-label", 58 | "missing-label", 59 | "blank-label", 60 | "duplicate-label", 61 | "incorrect-label", 62 | "blank-row", 63 | "primary-key-error", 64 | "foreign-key-error", 65 | "extra-cell", 66 | "missing-cell", 67 | "type-error", 68 | "constraint-error", 69 | "unique-error" 70 | ], 71 | "partial": false, 72 | "errors": [], 73 | "stats": { 74 | "errors": 0 75 | }, 76 | "valid": true 77 | } 78 | ], 79 | "stats": { 80 | "errors": 0, 81 | "tasks": 1 82 | }, 83 | "valid": true 84 | } 85 | -------------------------------------------------------------------------------- /livemark/program/merge.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import typer 4 | import atexit 5 | import tempfile 6 | from ..server import Server 7 | from ..project import Project 8 | from ..document import Document 9 | from .main import program 10 | from .. import settings 11 | from .. import errors 12 | from . import common 13 | 14 | 15 | @program.command(name="merge") 16 | def program_merge( 17 | source: str = common.source, 18 | config: str = common.config, 19 | print: bool = common.print, 20 | diff: bool = common.diff, 21 | live: bool = common.live, 22 | host: str = common.host, 23 | port: str = common.port, 24 | ): 25 | """Merge Markdown file into itself.""" 26 | 27 | try: 28 | # Handle config 29 | if not config: 30 | if os.path.exists(settings.DEFAULT_CONFIG): 31 | config = settings.DEFAULT_CONFIG 32 | 33 | # Handle source 34 | if not source and not config: 35 | if os.path.exists(settings.DEFAULT_SOURCE): 36 | source = settings.DEFAULT_SOURCE 37 | 38 | # Validate project 39 | if not source and not config: 40 | message = 'Project without "source" requires "config"' 41 | raise errors.Error(message) 42 | 43 | # Create project 44 | project = Project( 45 | source, 46 | format="md", 47 | config=config, 48 | ) 49 | 50 | # Normal mode 51 | if not live: 52 | output = project.build(diff=diff, print=print) 53 | if output and diff: 54 | sys.exit(1) 55 | sys.exit(0) 56 | 57 | # Live mode 58 | if not project.document: 59 | message = 'Live mode requries the "source" argument' 60 | raise errors.Error(message) 61 | atexit.register(project.document.build) 62 | with tempfile.NamedTemporaryFile(suffix=".html") as file: 63 | server = Server(Document(source, target=file.name).project) 64 | server.start(host=host, port=port, file=file.name) 65 | 66 | except Exception as exception: 67 | typer.secho(str(exception), err=True, fg=typer.colors.RED, bold=True) 68 | sys.exit(1) 69 | -------------------------------------------------------------------------------- /livemark.yaml: -------------------------------------------------------------------------------- 1 | brand: 2 | text: Livemark 3 | about: 4 | text: Data presentation framework for Python that generates static sites from extended Markdown with interactive charts, tables, scripts, and other features 5 | site: 6 | favicon: assets/logo.png 7 | styles: 8 | - style.css 9 | github: 10 | user: frictionlessdata 11 | repo: livemark 12 | counter: 13 | type: plausible 14 | code: livemark.frictionlessdata.io 15 | topics: 16 | selector: h2 17 | links: 18 | items: 19 | - name: Frictionless 20 | path: https://frictionlessdata.io 21 | - name: Templates 22 | path: https://github.com/search?q=topic%3Alivemark+topic%3Atemplate 23 | - name: Support 24 | path: https://discord.com/channels/695635777199145130/695635777199145133 25 | pages: 26 | items: 27 | - path: index 28 | name: Introduction 29 | - name: Getting Started 30 | items: 31 | - path: pages/getting-started/installation 32 | - path: pages/getting-started/configuration 33 | - path: pages/getting-started/using-markdown 34 | - path: pages/getting-started/starting-project 35 | - path: pages/getting-started/building-website 36 | - path: pages/getting-started/troubleshooting 37 | - path: pages/getting-started/contributing 38 | from: CONTRIBUTING.md 39 | - name: Guides and Tutorials 40 | items: 41 | - path: pages/guides-and-tutorials/data-journalism 42 | - path: pages/guides-and-tutorials/software-education 43 | - path: pages/guides-and-tutorials/python-development 44 | - name: Feature Reference 45 | items: 46 | - path: pages/feature-reference/general 47 | - path: pages/feature-reference/markdown 48 | - path: pages/feature-reference/appearance 49 | - path: pages/feature-reference/navigation 50 | - path: pages/feature-reference/blogging 51 | - path: pages/feature-reference/others 52 | - name: Plugin System 53 | items: 54 | - path: pages/plugin-system/adding-plugin 55 | - path: pages/plugin-system/writing-plugin 56 | - path: pages/plugin-system/architecture 57 | - path: pages/plugin-system/reference 58 | - name: Universe 59 | items: 60 | - path: pages/universe/projects 61 | - path: pages/universe/plugins 62 | - path: pages/release 63 | - path: pages/forum 64 | - path: blog/index 65 | redirect: 66 | items: 67 | - prev: pages/getting-started 68 | next: pages/getting-started/installation 69 | -------------------------------------------------------------------------------- /pages/feature-reference/appearance.md: -------------------------------------------------------------------------------- 1 | # Appearance 2 | 3 | ## Brand 4 | 5 | This feature adds a project name at the top left corner: 6 | 7 | ```yaml image 8 | path: ../../assets/brand.png 9 | width: unset 10 | height: unset 11 | class: border 12 | ``` 13 | 14 | > livemark.yaml 15 | 16 | ```yaml 17 | brand: 18 | text: Livemark 19 | ``` 20 | 21 | ## Notes 22 | 23 | By default, this feature shows a last updated datetime and a link to Livemark: 24 | 25 | ```yaml image 26 | path: ../../assets/notes.png 27 | width: unset 28 | height: unset 29 | class: border 30 | ``` 31 | 32 | > livemark.yaml 33 | 34 | ```yaml 35 | notes: 36 | format: %Y-%m-%d 37 | ``` 38 | 39 | ## Rating 40 | 41 | Based on the "Github" feature setting it will show a repository's star rating: 42 | 43 | ```yaml image 44 | path: ../../assets/rating.png 45 | width: unset 46 | height: unset 47 | class: border 48 | ``` 49 | 50 | > livemark.yaml 51 | 52 | ```yaml 53 | rating: 54 | type: start 55 | ``` 56 | 57 | ## About 58 | 59 | This feature adds information about the project or the page to the top right corner: 60 | 61 | ```yaml image 62 | path: ../../assets/about.png 63 | width: unset 64 | height: unset 65 | class: border 66 | ``` 67 | 68 | > livemark.yaml 69 | 70 | ```yaml 71 | about: 72 | text: Livemark is a Python static site generator... 73 | ``` 74 | 75 | ## Display 76 | 77 | This feature gives an ability to print a page, increase/decrease font size readability, and use a "Back to top" button: 78 | 79 | ```yaml image 80 | path: ../../assets/display.png 81 | width: unset 82 | height: unset 83 | class: border 84 | ``` 85 | 86 | ## Mobile 87 | 88 | This feature provides a mobile version of the site: 89 | 90 | ```yaml image 91 | path: ../../assets/mobile.png 92 | width: unset 93 | height: unset 94 | class: border 95 | ``` 96 | 97 | ## Source 98 | 99 | This features shows a Markdown source of a section on the "Source" heading button, which is available on hover. It is really useful to show the underlying source code for data visualisations: 100 | 101 | ```yaml image 102 | path: ../../assets/source.png 103 | width: 100% 104 | height: unset 105 | class: border 106 | ``` 107 | 108 | ## News 109 | 110 | This adds news to the top of the site: 111 | 112 | ```yaml image 113 | path: ../../assets/news.png 114 | width: 100% 115 | height: unset 116 | class: border 117 | ``` 118 | 119 | > livemark.yaml 120 | 121 | ```yaml 122 | news: 123 | text: It's test news 124 | ``` 125 | -------------------------------------------------------------------------------- /livemark/plugins/table/plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | from frictionless import Resource, Detector 4 | from ...plugin import Plugin 5 | 6 | 7 | class TablePlugin(Plugin): 8 | identity = "table" 9 | priority = 60 10 | 11 | # Process 12 | 13 | def process_document(self, document): 14 | self.__count = 0 15 | 16 | def process_snippet(self, snippet): 17 | if self.document.format == "html": 18 | if snippet.type == "table" and snippet.lang in ["yaml", "json"]: 19 | if snippet.lang == "yaml": 20 | spec = yaml.safe_load(str(snippet.input).strip()) 21 | if snippet.lang == "json": 22 | spec = json.loads(str(snippet.input).strip()) 23 | detector = Detector(field_float_numbers=True) 24 | with Resource(spec.pop("data", []), detector=detector) as resource: 25 | header = resource.header 26 | rows = resource.read_rows() 27 | columns = spec.get("columns", []) 28 | if not columns: 29 | for label in header: 30 | columns.append({"data": label}) 31 | width = spec.pop("width", "100%") 32 | if isinstance(width, int): 33 | width = f"{width}px" 34 | spec.setdefault("columnDefs", []) 35 | spec["columnDefs"].append( 36 | {"targets": "_all", "orderSequence": ["desc", "asc"]} 37 | ) 38 | spec = json.dumps(spec, ensure_ascii=False) 39 | spec = spec.replace("'", "\\'") 40 | self.__count += 1 41 | card = snippet.props.get("card") 42 | elem = f"livemark-table-{self.__count}" 43 | if card: 44 | elem += "-card" 45 | snippet.output = ( 46 | self.read_asset( 47 | "markup.html", 48 | card=card, 49 | elem=elem, 50 | spec=spec, 51 | rows=rows, 52 | columns=columns, 53 | width=width, 54 | ) 55 | + "\n" 56 | ) 57 | 58 | def process_markup(self, markup): 59 | if self.__count: 60 | url = "https://cdn.datatables.net/1.11.3" 61 | markup.add_style(f"{url}/css/jquery.dataTables.css") 62 | markup.add_script(f"{url}/js/jquery.dataTables.js") 63 | -------------------------------------------------------------------------------- /livemark/program/run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import typer 4 | import marko 5 | import subprocess 6 | from marko import md_renderer 7 | from ..project import Project 8 | from ..snippet import Snippet 9 | from .main import program 10 | from .. import settings 11 | from . import common 12 | 13 | 14 | # NOTE: 15 | # It's an initial implementation that works directly with markdown renderer 16 | # We need to implement it properly using a normal system flow and TaskPlugin 17 | 18 | 19 | @program.command(name="run") 20 | def program_run( 21 | task: str = common.task, 22 | config: str = common.config, 23 | ): 24 | """Run a Livemark task""" 25 | 26 | try: 27 | # Handle config 28 | if not config: 29 | if os.path.exists(settings.DEFAULT_CONFIG): 30 | config = settings.DEFAULT_CONFIG 31 | 32 | # Create project 33 | project = Project(config=config) 34 | project.read() 35 | project.process() 36 | 37 | # Extract snippets 38 | snippets = [] 39 | for document in project.documents: 40 | document.read() 41 | markdown = marko.Markdown(renderer=TaskRenderer) 42 | output = markdown.parse(document.content) 43 | markdown.renderer.snippets = [] 44 | markdown.render(output) 45 | snippets.extend(markdown.renderer.snippets) 46 | 47 | # List tasks 48 | if not task: 49 | for snippet in snippets: 50 | typer.secho(snippet.props["id"]) 51 | sys.exit(0) 52 | 53 | # Execute tasks 54 | scope = {} 55 | for snippet in snippets: 56 | if snippet.props["id"].startswith(task): 57 | if snippet.lang == "bash": 58 | subprocess.run(snippet.input, shell=True) 59 | elif snippet.lang == "python": 60 | exec(snippet.input, scope) 61 | 62 | except Exception as exception: 63 | typer.secho(str(exception), err=True, fg=typer.colors.RED, bold=True) 64 | sys.exit(1) 65 | 66 | 67 | # Internal 68 | 69 | 70 | class TaskRenderer(md_renderer.MarkdownRenderer): 71 | # Render 72 | 73 | def render_fenced_code(self, element): 74 | input = self.render_children(element).strip() 75 | header = [element.lang] + element.extra.split() 76 | snippet = Snippet(input, header=header) 77 | task_id = snippet.props.get("id") 78 | if snippet.type == "task" and task_id: 79 | self.snippets.append(snippet) 80 | return super().render_fenced_code(element) 81 | -------------------------------------------------------------------------------- /livemark/plugins/search/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | const prepare = () => { 3 | const searchParams = new URLSearchParams(window.location.search); 4 | const query = searchParams.get('query') || '' 5 | if (query.length >= 3) { 6 | searchInput.value = query 7 | } 8 | } 9 | const search = () => { 10 | unhighlight() 11 | query = searchInput.value 12 | searchOutput.innerHTML = '' 13 | searchOutput.style.visibility = 'hidden' 14 | const searchParams = new URLSearchParams(window.location.search); 15 | if (query.length < 3) return 16 | const results = searchIndex.search(query) 17 | if (!results.length) return 18 | searchParams.set('query', query) 19 | const newRelativePathQuery = window.location.pathname + '?' + searchParams.toString(); 20 | history.pushState(null, '', newRelativePathQuery); 21 | const elements = [] 22 | for (const result of results) { 23 | const item = searchItems[result.ref] 24 | const link = `${item.relpath}.html` 25 | const cls = window.location.pathname === link ? 'class="active"' : '' 26 | elements.push(`
  • ${item.name}
  • `) 27 | } 28 | searchOutput.innerHTML = `
      \n${elements.join('\n')}\n
    ` 29 | searchOutput.style.visibility = 'visible' 30 | highlight() 31 | } 32 | const highlight = () => { 33 | const stem = lunr.stemmer(new lunr.Token(query)).str 34 | $('#livemark-main').highlight(stem, {className: 'livemark-search-found'}); 35 | setTimeout(() => { 36 | $(window).scrollTo($('.livemark-search-found').first(), 1000) 37 | }, 1000) 38 | } 39 | const unhighlight = () => { 40 | $('#livemark-main').unhighlight({className: 'livemark-search-found'}); 41 | } 42 | const searchItems = { 43 | {% for item in plugin.items %} 44 | '{{ item.path }}': { 45 | 'name': '{{ item.name }}', 46 | 'path': '{{ item.path }}', 47 | 'relpath': '{{ item.relpath }}', 48 | 'text': {{ item.text | striptags | tojson }}, 49 | }, 50 | {% endfor %} 51 | }; 52 | const searchIndex = lunr(function () { 53 | this.ref("path") 54 | this.field("name", { boost: 10 }) 55 | this.field("text") 56 | for (const item of Object.values(searchItems)) { 57 | this.add(item) 58 | } 59 | }); 60 | const searchOutput = document.getElementById('livemark-search-output') 61 | const searchInput = document.getElementById('livemark-search-input') 62 | searchInput.addEventListener('input', search) 63 | prepare() 64 | search() 65 | }); 66 | -------------------------------------------------------------------------------- /livemark/config.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import deepmerge 3 | import jsonschema 4 | from copy import deepcopy 5 | from .system import system 6 | from . import helpers 7 | from . import errors 8 | 9 | 10 | class Config(dict): 11 | """Livemark config 12 | 13 | Parameters: 14 | source (str): path to the config source 15 | 16 | """ 17 | 18 | def __init__(self, source=None): 19 | status = {} 20 | 21 | # Load config 22 | if isinstance(source, str): 23 | self.update(yaml.safe_load(helpers.read_file(source))) 24 | elif source: 25 | self.update(source) 26 | source = None 27 | 28 | # Process config 29 | for key, value in list(self.items()): 30 | if isinstance(value, bool): 31 | status[key] = value 32 | del self[key] 33 | 34 | # Validate config 35 | for Plugin in system.Plugins.values(): 36 | if self.get(Plugin.identity) and Plugin.validity: 37 | validator = jsonschema.Draft7Validator(Plugin.validity) 38 | for error in validator.iter_errors(self[Plugin.identity]): 39 | message = f'Invalid "{Plugin.identity}" config: {error.message}' 40 | raise errors.Error(message) 41 | 42 | # Set attributes 43 | self.__source = source 44 | self.__status = status 45 | 46 | @property 47 | def source(self): 48 | """Path of the config source 49 | 50 | Returns: 51 | str?: source 52 | """ 53 | return self.__source 54 | 55 | @property 56 | def status(self): 57 | """Mapping of plugin status 58 | 59 | Returns: 60 | dict: status 61 | """ 62 | return self.__status 63 | 64 | # Helpers 65 | 66 | def to_copy(self): 67 | """Create a copy 68 | 69 | Returns: 70 | Config: config copy 71 | """ 72 | return deepcopy(self) 73 | 74 | def to_dict(self): 75 | """Create a dict 76 | 77 | Returns: 78 | dict: config dict 79 | """ 80 | return deepcopy(dict(self)) 81 | 82 | def to_merge(self, mapping): 83 | """Create a merge 84 | 85 | Parameters: 86 | mapping (dict): dictionary to merge 87 | 88 | Returns: 89 | Config: config merge 90 | """ 91 | source = {} 92 | deepmerge.always_merger.merge(source, self) 93 | deepmerge.always_merger.merge(source, mapping) 94 | for key, value in self.status.items(): 95 | source[key] = value 96 | return Config(source) 97 | -------------------------------------------------------------------------------- /livemark/plugins/html/renderer.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from marko import html_renderer 3 | from marko.inline import RawText 4 | from marko.block import FencedCode 5 | from ...snippet import Snippet 6 | 7 | 8 | # NOTE: 9 | # Move all the `script/task` related code to corresponding plugins 10 | 11 | 12 | class HtmlRenderer(html_renderer.HTMLRenderer): 13 | # Render 14 | 15 | def render_fenced_code(self, element): 16 | input = element.children[0].children 17 | header = [element.lang] + element.extra.split() 18 | snippet = Snippet(input, header=header) 19 | snippet.process(self.document) 20 | 21 | # Script 22 | if snippet.type == "script" and snippet.output is not None: 23 | # Remove target 24 | if self.document.format == "md": 25 | index = self.root_node.children.index(element) 26 | if len(self.root_node.children) > index + 1: 27 | item = self.root_node.children[index + 1] 28 | if isinstance(item, FencedCode): 29 | del self.root_node.children[index + 1] 30 | 31 | # Return output 32 | output = super().render_fenced_code(element) 33 | if snippet.output: 34 | target = copy(element) 35 | target.lang = snippet.props.get("output", "markup") 36 | target.extra = "" 37 | target.children = [RawText(snippet.output)] 38 | output += "\n" 39 | output += super().render_fenced_code(target) 40 | 41 | # Task 42 | elif snippet.type == "task" and snippet.props.get("id"): 43 | # Return output 44 | task = snippet.props["id"] 45 | target = copy(element) 46 | target.lang = "bash" 47 | target.extra = "" 48 | target.children = [RawText(f"$ livemark run {task}")] 49 | output = '
    ' 50 | output += super().render_fenced_code(target) 51 | output += "\n" 52 | output += super().render_fenced_code(element) 53 | output += "
    " 54 | 55 | # Others 56 | elif snippet.output is not None: 57 | output = snippet.output 58 | 59 | # Default 60 | else: 61 | output = super().render_fenced_code(element) 62 | 63 | # Container 64 | items = [] 65 | for name, value in snippet.props.items(): 66 | items.append(f'data-{name}="{value}"') 67 | output = f"
    {output}
    " 68 | 69 | return output 70 | 71 | 72 | class HtmlExtension: 73 | renderer_mixins = [HtmlRenderer] 74 | -------------------------------------------------------------------------------- /livemark/system.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pkgutil 4 | import importlib 5 | from cached_property import cached_property 6 | from .plugin import Plugin 7 | from . import helpers 8 | from . import errors 9 | 10 | 11 | class System: 12 | """System for plugin management 13 | 14 | This class provides access to Livemark plugins. 15 | It's available as `livemark.system` singletone. 16 | 17 | """ 18 | 19 | @cached_property 20 | def Plugins(self): 21 | """Registered plugins 22 | 23 | Returns: 24 | dict: plugins mapping 25 | """ 26 | Plugins = {} 27 | modules = [] 28 | if "" not in sys.path: 29 | sys.path.insert(0, "") 30 | for item in ["livemark.plugins", "plugins"]: 31 | if importlib.util.find_spec(item): 32 | module = importlib.import_module(item) 33 | dirname = os.path.dirname(module.__file__) 34 | for _, name, _ in pkgutil.iter_modules([dirname]): 35 | module = importlib.import_module(f"{item}.{name}") 36 | modules.append(module) 37 | for item in pkgutil.iter_modules(): 38 | if item.name.startswith("livemark_") or item.name == "plugin": 39 | module = importlib.import_module(item.name) 40 | modules.append(module) 41 | for module in modules: 42 | for Class in helpers.extract_classes(module, Plugin): 43 | if Class.identity in Plugins: 44 | raise errors.Error(f"Plugin name conflict: {Class.identity}") 45 | Plugins[Class.identity] = Class 46 | return Plugins 47 | 48 | # Manage 49 | 50 | def iterate(self): 51 | """Iterate plugins by priority 52 | 53 | Returns: 54 | type[]: list of plugin classes 55 | """ 56 | objects = self.Plugins.values() 57 | return helpers.order_objects(objects, "priority") 58 | 59 | def register(self, Plugin): 60 | """Register a plugin 61 | 62 | Parameters: 63 | Plugin (type): a plugin class to register 64 | """ 65 | if Plugin.identity in self.Plugins: 66 | raise errors.Error(f"Plugin name conflict: {Plugin.identity}") 67 | self.Plugins[Plugin.identity] = Plugin 68 | 69 | def deregister(self, Plugin): 70 | """Deregister a plugin 71 | 72 | Parameters: 73 | Plugin (type): a plugin class to register 74 | """ 75 | if Plugin.identity not in self.Plugins: 76 | raise errors.Error(f"Not registered plugin: {Plugin.identity}") 77 | del self.Plugins[Plugin.identity] 78 | 79 | 80 | system = System() 81 | -------------------------------------------------------------------------------- /livemark/plugins/script/plugin.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from ...plugin import Plugin 3 | from ... import helpers 4 | 5 | 6 | # NOTE: 7 | # Consider making the scope publicly available so other plugins 8 | # would be able to use it. For example, creating a table/chart/etc from a var 9 | 10 | 11 | class ScriptPlugin(Plugin): 12 | identity = "script" 13 | priority = 60 14 | validity = { 15 | "type": "object", 16 | "properties": { 17 | "basepath": {"type": "string"}, 18 | }, 19 | } 20 | 21 | # Context 22 | 23 | @property 24 | def basepath(self): 25 | return self.config.get("basepath") 26 | 27 | # Process 28 | 29 | def __init__(self, document): 30 | super().__init__(document) 31 | self.__store = [] 32 | self.__scope = {} 33 | 34 | def process_document(self, config): 35 | self.__index = 0 36 | 37 | def process_snippet(self, snippet): 38 | if snippet.type == "script" and snippet.lang in ["python", "bash"]: 39 | # Acquire cache 40 | cache = helpers.list_setdefault( 41 | self.__store, 42 | self.__index, 43 | default={}, 44 | ) 45 | 46 | # Invalidate cache 47 | if cache: 48 | hit = cache["lang"] == snippet.lang and cache["input"] == snippet.input 49 | if not hit: 50 | cache = {} 51 | self.__store = self.__store[: self.__index] 52 | self.__store.append(cache) 53 | 54 | # Populate cache 55 | if not cache: 56 | # Bash 57 | if snippet.lang == "bash": 58 | try: 59 | output = subprocess.check_output( 60 | snippet.input, 61 | shell=True, 62 | cwd=self.basepath, 63 | ) 64 | output = output.decode().strip() 65 | except Exception as exception: 66 | output = exception.output.decode().strip() 67 | 68 | # Python 69 | elif snippet.lang == "python": 70 | with helpers.capture_stdout(cwd=self.basepath) as stdout: 71 | exec(snippet.input, self.__scope) 72 | output = stdout.getvalue().strip() 73 | 74 | # General 75 | output = "\n".join(line.rstrip() for line in output.splitlines()) 76 | cache["lang"] = snippet.lang 77 | cache["input"] = snippet.input 78 | cache["output"] = output 79 | 80 | # Apply cache 81 | self.__index += 1 82 | snippet.output = cache["output"] 83 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import io 3 | from setuptools import setup, find_packages 4 | 5 | 6 | # Helpers 7 | 8 | 9 | def read(*paths): 10 | """Read a text file.""" 11 | basedir = os.path.dirname(__file__) 12 | fullpath = os.path.join(basedir, *paths) 13 | contents = io.open(fullpath, encoding="utf-8").read().strip() 14 | return contents 15 | 16 | 17 | # Prepare 18 | 19 | 20 | PACKAGE = "livemark" 21 | NAME = PACKAGE.replace("_", "-") 22 | TESTS_REQUIRE = [ 23 | "mypy", 24 | "black", 25 | # TODO: remove after the fix 26 | # https://github.com/klen/pylama/issues/224 27 | "pyflakes==2.4.0", 28 | "pylama", 29 | "pytest", 30 | "ipython", 31 | "pytest-cov", 32 | "pytest-vcr", 33 | "pytest-only", 34 | ] 35 | EXTRAS_REQUIRE = { 36 | "dev": TESTS_REQUIRE, 37 | } 38 | INSTALL_REQUIRES = [ 39 | "attrs>=22.0", 40 | "marko==1.*", 41 | "pyyaml>=5.3", 42 | "jinja2>=3.0", 43 | "pyquery==1.*", 44 | "deepmerge>=0.3", 45 | "gitpython>=3.1", 46 | "jsonschema>=2.5", 47 | "typer>=0.12", 48 | "livereload>=2.6", 49 | "giturlparse>=0.10", 50 | "cached_property>=1.5", 51 | "docstring-parser>=0.10", 52 | "frictionless[excel,json]>=4.0", 53 | ] 54 | README = read("README.md") 55 | VERSION = read(PACKAGE, "assets", "VERSION") 56 | PACKAGES = find_packages(exclude=["tests"]) 57 | ENTRY_POINTS = {"console_scripts": ["livemark = livemark.__main__:program"]} 58 | 59 | 60 | # Run 61 | 62 | 63 | setup( 64 | name=NAME, 65 | version=VERSION, 66 | packages=PACKAGES, 67 | include_package_data=True, 68 | install_requires=INSTALL_REQUIRES, 69 | tests_require=TESTS_REQUIRE, 70 | extras_require=EXTRAS_REQUIRE, 71 | entry_points=ENTRY_POINTS, 72 | zip_safe=False, 73 | long_description=README, 74 | long_description_content_type="text/markdown", 75 | description="Data presentation framework for Python that generates static sites from extended Markdown with interactive charts, tables, scripts, and other features.", 76 | author="Evgeny Karev", 77 | author_email="eskarev@gmail.com", 78 | url="https://github.com/frictionlessdata/livemark", 79 | license="MIT", 80 | keywords=[ 81 | "livemark", 82 | "markdown", 83 | "documentation", 84 | ], 85 | classifiers=[ 86 | "Development Status :: 4 - Beta", 87 | "Environment :: Console", 88 | "Intended Audience :: Developers", 89 | "License :: OSI Approved :: MIT License", 90 | "Operating System :: OS Independent", 91 | "Programming Language :: Python :: 3", 92 | "Programming Language :: Python :: 3.8", 93 | "Programming Language :: Python :: 3.9", 94 | "Programming Language :: Python :: 3.10", 95 | "Topic :: Software Development :: Libraries :: Python Modules", 96 | ], 97 | ) 98 | -------------------------------------------------------------------------------- /livemark/plugins/blog/plugin.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import glob 4 | from ...plugin import Plugin 5 | from ...document import Document 6 | from ... import helpers 7 | 8 | 9 | class BlogPlugin(Plugin): 10 | identity = "blog" 11 | validity = { 12 | "type": "object", 13 | "properties": { 14 | "author": {"type": "string"}, 15 | "image": {"type": "string"}, 16 | "description": {"type": "string"}, 17 | }, 18 | } 19 | 20 | # Context 21 | 22 | @property 23 | def path(self): 24 | path = self.document.project.config.get("blog", {}).get("path", "blog") 25 | if os.path.isdir(path): 26 | return path 27 | 28 | @property 29 | def relpath(self): 30 | path = f"{self.path}/index.html" 31 | return helpers.get_url_relpath(path, self.document.path) 32 | 33 | @property 34 | def items(self): 35 | items = [] 36 | if not self.path: 37 | return items 38 | index_path = "/".join([self.path, "index"]) 39 | for document in self.document.project.documents: 40 | if document.path.startswith(self.path) and document.path != index_path: 41 | relpath = helpers.get_url_relpath(document.path, self.document.path) 42 | items.append({"document": document, "relpath": relpath}) 43 | return items 44 | 45 | @property 46 | def author(self): 47 | return self.config.get("author") 48 | 49 | @property 50 | def image(self): 51 | return self.config.get("image") 52 | 53 | @property 54 | def description(self): 55 | return self.config.get("description", self.document.description) 56 | 57 | @property 58 | def date(self): 59 | if self.path: 60 | date = self.document.path.replace(f"{self.path}/", "") 61 | return "-".join(re.split(r"[/-]", date)[:3]) 62 | 63 | # Process 64 | 65 | @staticmethod 66 | def process_project(project): 67 | path = project.config.get("blog", {}).get("path", "blog") 68 | if os.path.isdir(path): 69 | index_default = os.path.join(os.path.dirname(__file__), "index.md") 70 | index_source = os.path.join(path, "index.md") 71 | if not os.path.isfile(index_source): 72 | helpers.copy_file(index_default, index_source) 73 | sources = glob.glob(f"{path}/**/*.md", recursive=True) 74 | for source in sorted(sources, reverse=True): 75 | if source != index_source: 76 | item = Document(source, project=project) 77 | project.documents.append(item) 78 | 79 | def process_markup(self, markup): 80 | markup.add_style("style.css") 81 | if self.author: 82 | markup.add_markup("markup.html", target="h1 + p", action="prepend") 83 | markup.query('a[href="/blog/index.html"]').parent().add_class("active") 84 | -------------------------------------------------------------------------------- /livemark/snippet.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import TYPE_CHECKING, Optional, List, Dict, Any 3 | 4 | if TYPE_CHECKING: 5 | from .document import Document 6 | 7 | 8 | # TODO: 9 | # We can parse json/yaml in advance for snippet-processing plugins 10 | 11 | 12 | class Snippet: 13 | """Livemark snippet 14 | 15 | Parameters: 16 | input: textual snippet for the snippet 17 | header: an array of the snippet's header 18 | 19 | """ 20 | 21 | def __init__(self, input: str, *, header: List[str]): 22 | lang = "" 23 | type = "" 24 | props = {} 25 | 26 | # Infer lang 27 | if len(header) >= 1: 28 | lang = header[0].lower() 29 | 30 | # Infer type/props 31 | for index, item in enumerate(header[1:]): 32 | if index == 0 and "=" not in item: 33 | type = item 34 | continue 35 | parts = item.split("=") 36 | name = parts[0].lower() 37 | value = parts[1] if len(parts) == 2 else True 38 | props[name] = value 39 | 40 | # Set attributes 41 | self.__input = input 42 | self.__header = header 43 | self.__output = None 44 | self.__lang = lang 45 | self.__type = type 46 | self.__props = props 47 | 48 | def __setattr__(self, name, value): 49 | if name == "output": 50 | self.__output = value 51 | elif name == "input": 52 | self.__input = value 53 | elif name == "lang": 54 | self.__lang = value 55 | elif name == "type": 56 | self.__type = value 57 | else: # default setter 58 | super().__setattr__(name, value) 59 | 60 | @property 61 | def input(self) -> str: 62 | """Snippet's input""" 63 | return self.__input 64 | 65 | @property 66 | def output(self) -> Optional[str]: 67 | """Snippet's output""" 68 | return self.__output 69 | 70 | @property 71 | def header(self) -> List[str]: 72 | """Snippet's header""" 73 | return self.__header 74 | 75 | @property 76 | def lang(self) -> str: 77 | """Snippet's lang""" 78 | return self.__lang 79 | 80 | @property 81 | def type(self) -> str: 82 | """Snippet's type 83 | 84 | Returns: 85 | str: type 86 | """ 87 | return self.__type 88 | 89 | @property 90 | def props(self) -> Dict[str, Any]: 91 | """Snippet's props""" 92 | return self.__props 93 | 94 | # Process 95 | 96 | def process(self, document: Document) -> None: 97 | """Process snippet 98 | 99 | Parameters: 100 | document: document having this snippet 101 | """ 102 | for plugin in document.plugins: 103 | plugin.process_snippet(self) 104 | -------------------------------------------------------------------------------- /livemark/plugins/pages/plugin.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from ...plugin import Plugin 3 | from ...document import Document 4 | from ... import helpers 5 | 6 | 7 | class PagesPlugin(Plugin): 8 | identity = "pages" 9 | priority = 70 10 | validity = { 11 | "type": "object", 12 | "properties": { 13 | "items": { 14 | "type": "array", 15 | "items": { 16 | "type": "object", 17 | "properties": { 18 | "name": {"type": "string"}, 19 | "path": {"type": "string"}, 20 | "items": {"type": "array"}, 21 | }, 22 | }, 23 | }, 24 | }, 25 | } 26 | 27 | # Context 28 | 29 | @property 30 | def current(self): 31 | return self.document.path 32 | 33 | @property 34 | def items(self): 35 | items = deepcopy(self.config.get("items", [])) 36 | for item in items: 37 | item["active"] = False 38 | subitems = item.get("items", []) 39 | 40 | # Handle nested 41 | for subitem in subitems: 42 | document = self.document.project.get_document(subitem["path"]) 43 | subitem.setdefault("name", document.get_plugin("site").name) 44 | subitem["active"] = False 45 | subitem["relpath"] = helpers.get_url_relpath( 46 | subitem["path"], self.current 47 | ) 48 | if subitem["path"] == self.current: 49 | item["active"] = True 50 | subitem["active"] = True 51 | 52 | # Handle top-level 53 | if not subitems: 54 | document = self.document.project.get_document(item["path"]) 55 | item.setdefault("name", document.get_plugin("site").name) 56 | item["relpath"] = helpers.get_url_relpath(item["path"], self.current) 57 | if item["path"] == self.current: 58 | item["active"] = True 59 | 60 | return items 61 | 62 | @property 63 | def flatten_items(self): 64 | return helpers.flatten_items(self.items, "items") 65 | 66 | # Process 67 | 68 | @staticmethod 69 | def process_project(project): 70 | items = project.config.get("pages", {}).get("items", []) 71 | for item in helpers.flatten_items(items, "items"): 72 | source = item.get("from", helpers.with_format(item["path"], "md")) 73 | target = helpers.with_format(item["path"], project.format) 74 | document = Document(source, target=target, project=project, path=item["path"]) 75 | project.documents.append(document) 76 | 77 | def process_markup(self, markup): 78 | if self.items: 79 | markup.add_style("style.css") 80 | markup.add_script("script.js") 81 | markup.add_markup("markup.html", target="#livemark-left") 82 | -------------------------------------------------------------------------------- /livemark/plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import inspect 3 | from jinja2 import Template 4 | 5 | 6 | # NOTE: 7 | # Consider cached Plugin.document/project_property if optimization is needed 8 | 9 | 10 | class Plugin: 11 | """Livemark plugin 12 | 13 | Parameters: 14 | document (Document): a document to which the plulgin belongs 15 | 16 | """ 17 | 18 | identity = "" 19 | """Plugin's name 20 | """ 21 | 22 | priority = 0 23 | """Plugin's processing priority 24 | """ 25 | 26 | validity = {} 27 | """Plugin's JSON Schema for config validation 28 | """ 29 | 30 | def __init__(self, document): 31 | self.__document = document 32 | 33 | @property 34 | def config(self): 35 | """Plugin's config (empty if not provided) 36 | 37 | Returns: 38 | dict: config 39 | """ 40 | return self.__document.config.get(self.identity, {}) 41 | 42 | @property 43 | def document(self): 44 | """Plugin's document it belongs to 45 | 46 | Returns: 47 | Document: document 48 | """ 49 | return self.__document 50 | 51 | # Process 52 | 53 | @staticmethod 54 | def process_project(project): 55 | """Process project 56 | 57 | Parameters: 58 | project (Project): a project to process 59 | """ 60 | pass 61 | 62 | def process_document(self, document): 63 | """Process document 64 | 65 | Parameters: 66 | document (Markup): a document to process 67 | """ 68 | pass 69 | 70 | def process_snippet(self, snippet): 71 | """Process snippet 72 | 73 | Parameters: 74 | snippet (Markup): a snippet to process 75 | """ 76 | pass 77 | 78 | def process_markup(self, markup): 79 | """Process markup 80 | 81 | Parameters: 82 | markup (Markup): a markup to process 83 | """ 84 | pass 85 | 86 | # Helpers 87 | 88 | def read_asset(self, *path, **context): 89 | """Read plugin's asset 90 | 91 | Parameters: 92 | path (str[]): paths to join 93 | context (dict): template variables 94 | 95 | Returns: 96 | str: a read asset 97 | """ 98 | dir = os.path.dirname(inspect.getfile(self.__class__)) 99 | path = os.path.join(dir, *path) 100 | context["plugin"] = self 101 | with open(path) as file: 102 | template = Template(file.read().strip(), trim_blocks=True) 103 | text = template.render(**context) 104 | return text 105 | 106 | @classmethod 107 | def check_status(cls, config): 108 | """Check whether the plugin is active in given config 109 | 110 | Parameters: 111 | config (Config): a config 112 | 113 | Returns: 114 | bool: whether active 115 | """ 116 | type = "external" if cls.__module__.startswith("livemark_") else "internal" 117 | internal = type == "internal" and config.status.get(cls.identity) is not False 118 | external = type == "external" and config.status.get(cls.identity) is True 119 | return internal or external 120 | -------------------------------------------------------------------------------- /.github/workflows/general.yaml: -------------------------------------------------------------------------------- 1 | name: general 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v*.*.* 9 | pull_request: 10 | branches: 11 | - main 12 | # schedule: 13 | # - cron: "0 3 * * *" 14 | 15 | jobs: 16 | # Test (Linux) 17 | 18 | test-linux: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | python-version: [3.8, 3.9, "3.10"] 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v2 26 | - name: Install Python 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install dependencies 31 | run: make install 32 | - name: Test software 33 | run: make test 34 | - name: Report coverage 35 | uses: codecov/codecov-action@v1 36 | 37 | # Test (MacOS) 38 | 39 | test-macos: 40 | runs-on: macos-latest 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v2 44 | - name: Install Python 45 | uses: actions/setup-python@v2 46 | with: 47 | python-version: 3.8 48 | - name: Install dependencies 49 | run: make install 50 | - name: Test software 51 | run: make test 52 | 53 | # Test (Windows) 54 | 55 | test-windows: 56 | runs-on: windows-latest 57 | steps: 58 | - name: Checkout repository 59 | uses: actions/checkout@v2 60 | - name: Install Python 61 | uses: actions/setup-python@v2 62 | with: 63 | python-version: 3.8 64 | - name: Install dependencies 65 | run: make install 66 | - name: Test software 67 | run: make test 68 | 69 | # Deploy 70 | 71 | deploy: 72 | if: github.event_name == 'push' 73 | runs-on: ubuntu-latest 74 | steps: 75 | - name: Checkout repository 76 | uses: actions/checkout@v2 77 | - name: Install Python 78 | uses: actions/setup-python@v2 79 | with: 80 | python-version: 3.8 81 | - name: Install and build site 82 | run: | 83 | echo '!**/*.html' >> .gitignore 84 | pip install -e . 85 | livemark build 86 | - name: Publush to Github Pages 87 | uses: stefanzweifel/git-auto-commit-action@v4 88 | with: 89 | branch: site 90 | create_branch: true 91 | push_options: "--force" 92 | 93 | # Release 94 | 95 | release: 96 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 97 | runs-on: ubuntu-latest 98 | needs: [test-linux, test-macos, test-windows] 99 | steps: 100 | - name: Checkout repository 101 | uses: actions/checkout@v2 102 | - name: Install Python 103 | uses: actions/setup-python@v2 104 | with: 105 | python-version: 3.8 106 | - name: Install dependencies 107 | run: | 108 | python -m pip install --upgrade pip 109 | pip install setuptools wheel 110 | - name: Build distribution 111 | run: | 112 | python setup.py sdist bdist_wheel 113 | - name: Publish to PYPI 114 | uses: pypa/gh-action-pypi-publish@master 115 | with: 116 | password: ${{ secrets.PYPI_API_KEY }} 117 | - name: Release to GitHub 118 | uses: softprops/action-gh-release@v1 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | -------------------------------------------------------------------------------- /pages/guides-and-tutorials/python-development.md: -------------------------------------------------------------------------------- 1 | # Python Development 2 | 3 | Livemark can be used in software development as a helper tool for working on Python projects. It provides an ability to create documentation sites and works as a task runner. 4 | 5 | ## Example 6 | 7 | Livemark's documentation site is written in Livemark: 8 | 9 | > https://livemark.frictionlessdata.io/ 10 | 11 | ```yaml image 12 | path: ../../assets/example.png 13 | width: 100% 14 | height: unset 15 | class: border 16 | ``` 17 | 18 | ## Prerequisites 19 | 20 | Create a virtual environment (optional): 21 | 22 | ```bash 23 | $ python3 -m venv .python 24 | $ source .python/bin/activate 25 | ``` 26 | 27 | And install livemark: 28 | 29 | ``` 30 | $ pip install livemark 31 | ``` 32 | 33 | ## Writing Docs 34 | 35 | Working on a documentation site is not really different to working on a regular Livemark project. The main difference will be having your documentation along side with the codebase itself. 36 | 37 | First of all, create: 38 | - `livemark.yaml` 39 | - `index.md` 40 | - `pages/contrib.md` (for example) 41 | 42 | Fill in your configuration file: 43 | 44 | > livemark.yaml 45 | 46 | ```yaml 47 | brand: 48 | text: My Project 49 | about: 50 | text: My project is a software 51 | site: 52 | favicon: assets/favicon.ico 53 | github: 54 | user: 55 | repo: 56 | topics: 57 | selector: h2 58 | links: 59 | items: 60 | - name: About Me 61 | path: https://personal.site 62 | pages: 63 | items: 64 | - name: Introduction 65 | path: index 66 | - path: pages/contrib 67 | ``` 68 | 69 | Run a livereload server locally: 70 | 71 | ```bash 72 | $ livemark start 73 | ``` 74 | 75 | When you are ready to publish your work, commit the changes and push it to Github. The only missing part now is enabling Github Pages: 76 | 77 | > https://guides.github.com/features/pages/ 78 | 79 | ```yaml image 80 | path: ../../assets/deploy.png 81 | width: 75% 82 | height: unset 83 | class: border 84 | ``` 85 | 86 | After this step your documentation portal will be up and running. 87 | 88 | ## Getting Tests 89 | 90 | > A special Livemark command for testing docs is under construction 91 | 92 | An important thing about Livemark is that it runs scripts in documents during processing. It means that it basically tests all your code snippets on every build. 93 | 94 | For example, our guide includes this code (with a built output): 95 | 96 | > guide.md 97 | 98 | ``` 99 | '''python script 100 | mylib.command('Hello World') 101 | ''' 102 | ''' 103 | Hello World 104 | ''' 105 | ``` 106 | 107 | Every time during the build the software will be tested. 108 | 109 | ## Running Tasks 110 | 111 | Livemark supports having tasks written in Markdown documents. For example, if we have a `contrib.md` section like this, we need to add this page to `livemark.yaml:pages.items` to make it work: 112 | 113 | > contrib.md 114 | 115 | ``` 116 | '''bash task id=test-lint 117 | echo 'Test Lint' 118 | ''' 119 | '''python task id=test-code 120 | print('Test Code') 121 | ''' 122 | ``` 123 | 124 | Use this command to get a list of available tasks: 125 | 126 | ```bash 127 | $ livemark run 128 | ``` 129 | ``` 130 | test-lint 131 | test-code 132 | ``` 133 | 134 | Execute all of the tests: 135 | 136 | ```bash 137 | $ livemark run test 138 | ``` 139 | ``` 140 | Test Link 141 | Test Code 142 | ``` 143 | 144 | Or run an arbitrary task: 145 | 146 | ```bash 147 | $ livemark run test-code 148 | ``` 149 | ``` 150 | Test Code 151 | ``` 152 | -------------------------------------------------------------------------------- /livemark/assets/documents/template.md: -------------------------------------------------------------------------------- 1 | --- 2 | brand: 3 | text: Livemark 4 | github: 5 | user: frictionlessdata 6 | repo: livemark 7 | links: 8 | items: 9 | - name: Documentation 10 | path: https://livemark.frictionlessdata.io/ 11 | # add other config options here or create livemark.yaml file 12 | --- 13 | 14 | # Welcome to Livemark 15 | 16 | > Edit `index.md` file to explore Livemark's features or just remove everything to start from a scratch. 17 | 18 | It's a template document created automatically to introduce Livemark. We will list here core [Livemark](https://livemark.frictionlessdata.io/) features and you can play with theme live editing the document. It's possible to use any standard Markdown features as well. 19 | 20 | ## Logic 21 | 22 | We can pre-process our markdown file using [Jinja](https://jinja.palletsprojects.com/): 23 | 24 | {% for car in frictionless.Resource('https://raw.githubusercontent.com/frictionlessdata/livemark/main/data/cars.csv').read_rows(size=5) %} 25 | - {{ car.brand }} {{ car.model }}: ${{ car.price }} 26 | {% endfor %} 27 | 28 | ## Table 29 | 30 | We can visualize our data as a table using [HandsOnTable](https://handsontable.com/): 31 | 32 | ```yaml table 33 | data: https://raw.githubusercontent.com/frictionlessdata/livemark/main/data/cars.csv 34 | width: 600 35 | order: 36 | - [3, 'desc'] 37 | columns: 38 | - data: type 39 | - data: brand 40 | - data: model 41 | - data: price 42 | - data: kmpl 43 | - data: bhp 44 | ``` 45 | 46 | ## Chart 47 | 48 | Another option is to draw a chart using [Vega](https://vega.github.io/vega-lite/): 49 | 50 | ```yaml chart 51 | data: 52 | url: https://raw.githubusercontent.com/frictionlessdata/livemark/main/data/cars.csv 53 | mark: circle 54 | selection: 55 | brush: 56 | type: interval 57 | encoding: 58 | x: 59 | type: quantitative 60 | field: kmpl 61 | scale: 62 | domain: [12,25] 63 | y: 64 | type: quantitative 65 | field: price 66 | scale: 67 | domain: [100,900] 68 | color: 69 | condition: 70 | selection: brush 71 | field: type 72 | type: nominal 73 | value: grey 74 | size: 75 | type: quantitative 76 | field: bhp 77 | width: 500 78 | height: 300 79 | ``` 80 | 81 | ## Script 82 | 83 | Moreover, we can execute scripts in [Python](https://www.python.org/)/[Bash](https://www.gnu.org/software/bash/): 84 | 85 | ```python script 86 | for number in range(1, 6): 87 | print(f'Hello World #{number}!') 88 | ``` 89 | 90 | ## Markup 91 | 92 | Markdown is not enough? Finally, let's add some markup with [Bootstrap](https://getbootstrap.com/): 93 | 94 | ```html markup 95 |
    96 |
    97 |
    98 |
    99 | 100 |
    101 |

    Data Package

    102 |

    A simple container format for describing a coherent collection of data in a single package.

    103 |
    104 |
    105 |
    106 | 107 |
    108 |

    Data Resource

    109 |

    A simple format to describe and package a single data resource such as a individual table or file.

    110 |
    111 |
    112 |
    113 | 114 |
    115 |

    Table Schema

    116 |

    A simple format to declare a schema for tabular data. The schema is designed to be expressible in JSON.

    117 |
    118 |
    119 |
    120 |
    121 |
    122 | ``` 123 | --------------------------------------------------------------------------------