├── .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 | {{ resource.description_text or 'No description provided' }}
4 |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 |Source: {{ task.source }}
4 |Type: {{ task.type }}
5 |Steps: {{ task.steps }}
6 | {% endfor %} 7 |{{ package.description_text or 'No description provided' }}
4 |{{ resource.description_text or 'No description provided' }}
8 | {% endfor %} 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 |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 |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 |{{ row.description or 'Description is not provided'}}
13 |14 | 15 | Github 16 | 17 |
18 |{{ row.description or 'Description is not provided'}}
13 |14 | 15 | Github 16 | 17 |
18 || {{ column.title or column.name or column.data }} | 8 | {% endfor %} 9 |
|---|
| {{ row[column.data] if row[column.data] is not none else '' }} | 16 | {% endfor %} 17 |
| {{ column.title or column.name or column.data }} | 24 | {% endfor %} 25 |
${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 |
--------------------------------------------------------------------------------