diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..046000b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,27 @@ +name: website + +on: push + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Generate Chisai + run: python3 src/build.py --prod + #- + #name: Create cname file + # run: echo 'mydomain.com' > build/CNAME + - + name: Deploy to GitHub Pages + if: success() + uses: crazy-max/ghaction-github-pages@v2 + with: + target_branch: gh-pages + build_dir: build + keep_history: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ddff68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/__pycache__ +/build +kantan.sh diff --git a/01-01-2021.html b/01-01-2021.html deleted file mode 100644 index 52dea27..0000000 --- a/01-01-2021.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - Happy new year! | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - 01-01-2021 -

Happy new year!

-

Happy new year to all of you!

- -
-
- - - \ No newline at end of file diff --git a/02-01-2021.html b/02-01-2021.html deleted file mode 100644 index 8e5fb41..0000000 --- a/02-01-2021.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - A lady laying down | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - 02-01-2021 -

A lady laying down

-

This art is by Zeen Chin.

-

Art by Zeen Chin

- -
-
- - - \ No newline at end of file diff --git a/05-01-2021.html b/05-01-2021.html deleted file mode 100644 index 9e2a15e..0000000 --- a/05-01-2021.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - A pensive blue man | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - 05-01-2021 -

A pensive blue man

-

This is from Watchen by Dave Gibbons.

-

Blue man thinking by Dave Gibbons

- -
-
- - - \ No newline at end of file diff --git a/08-01-2021.html b/08-01-2021.html deleted file mode 100644 index 7a0ba87..0000000 --- a/08-01-2021.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - A strange looking city | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - 08-01-2021 -

A strange looking city

-

This city is by François Schuiten.

-

Strange floating city by François Schuiten

- -
-
- - - \ No newline at end of file diff --git a/12-01-2021.html b/12-01-2021.html deleted file mode 100644 index e29b442..0000000 --- a/12-01-2021.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - Chihiro having dinner | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - 12-01-2021 -

Chihiro having dinner

-

A still image from the movie Chihiro by Studio Ghibli.

-

Chihiro having dinner

- -
-
- - - \ No newline at end of file diff --git a/23-01-2021.html b/23-01-2021.html deleted file mode 100644 index f0afb28..0000000 --- a/23-01-2021.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - Kaguya dancing | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - 23-01-2021 -

Kaguya dancing

-

A still image from the movie The Tale of Princess Kaguya.

-

Kaguyra Dancing

- -
-
- - - \ No newline at end of file diff --git a/23-05-2021.html b/23-05-2021.html deleted file mode 100644 index bda6ce1..0000000 --- a/23-05-2021.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - Simple Date | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - 23-05-2021 -

Simple Date

-

This is a post with a single date. This date will be used both for listing and for the url.

- -
-
- - - \ No newline at end of file diff --git a/24-12-2021.html b/24-12-2021.html deleted file mode 100644 index 85c3e36..0000000 --- a/24-12-2021.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - Merry Christmas! | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - 24-12-2021 -

Merry Christmas!

-

Merry Christmas to all of you! I had a lovely dinner and got super socks from my partner!

- -
-
- - - \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a2726cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) + +Copyright © 2021 Thomasorus + +This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. + +Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: + +1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. + +2. The User is one of the following: +a. An individual person, laboring for themselves +b. A non-profit organization +c. An educational institution +d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor + +3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. + +4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..46d44ba --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +![Chisai logo](medias/chisai_square.png) + +# Chīsai! + +> A small website generator, editable and hosted on github, with automatic deployment! + +## Purpose and mission + +Chīsai is aimed at empowering people who need a free solution to have their own small space on the internet without falling into the CMS install and maintenance crazyness. All content is in markdown files, in folders, and the user can edit, upload images and host on github. + +Chīsai answers three very important things for me when it comes to websites: + +1. It should be easy to modify by someone else +2. It should be durable and long lasting +3. Each person should own the code of its website + +## Who can use it? + +Chīsai is a free to use by individuals and organizations that do not operate by capitalist principles. For more information see the [license](LICENSE) file. + +## How does it work? + +You can see a demo [here](https://thomasorus.github.io/Chisai/). It's quite simple: + +- Each section is a folder with files named as the dates of their publication. +- Each section has it's parent section that regroups all the files inside. +- All sections 5 latest entries are regrouped on the home page with a link to see the full content. +- Content is edited using the Markdown syntax (the same you can find on Discord or Slack). +- Edit the content (create folders, files, edit files) directly on github, here! + +That's all! + +## How to use it? + +See the [wiki](https://github.com/Thomasorus/Chisai/wiki) for extensive guides for everything! + +## Can I self-host it? + +Yes of course! Chisai only produces HTML. You can add the generated website on your own server. Even better: if your server has Python, you can rebuild the website direcly on your server. Be careful that you might have to reconfigure some things to make it work. + + + diff --git a/art.html b/art.html deleted file mode 100644 index 9da3d11..0000000 --- a/art.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - Art | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - -

Art

-

In this section I list art pieces that I enjoy. Creating pages in this art section is very simple: just add a new file and the generator will create them instantly!

- -
-
- - - \ No newline at end of file diff --git a/art/02-01-2021.md b/art/02-01-2021.md new file mode 100644 index 0000000..4dc1f5c --- /dev/null +++ b/art/02-01-2021.md @@ -0,0 +1,5 @@ +# A lady laying down + +This art is by [Zeen Chin](https://twitter.com/zeen_chin). + +![Art by Zeen Chin](lady.jpeg) \ No newline at end of file diff --git a/art/05-01-2021.md b/art/05-01-2021.md new file mode 100644 index 0000000..0d05c6c --- /dev/null +++ b/art/05-01-2021.md @@ -0,0 +1,5 @@ +# A pensive blue man + +This is from Watchen by Dave Gibbons. + +![Blue man thinking by Dave Gibbons](blue-man.png) \ No newline at end of file diff --git a/art/08-01-2021.md b/art/08-01-2021.md new file mode 100644 index 0000000..38dde0c --- /dev/null +++ b/art/08-01-2021.md @@ -0,0 +1,5 @@ +# A strange looking city + +This city is by François Schuiten. + +![Strange floating city by François Schuiten](city.jpeg) \ No newline at end of file diff --git a/art/12-01-2021.md b/art/12-01-2021.md new file mode 100644 index 0000000..53b1422 --- /dev/null +++ b/art/12-01-2021.md @@ -0,0 +1,5 @@ +# Chihiro having dinner + +A still image from the movie Chihiro by Studio Ghibli. + +![Chihiro having dinner](chihiro.jpg) \ No newline at end of file diff --git a/art/23-01-2021.md b/art/23-01-2021.md new file mode 100644 index 0000000..a8b6222 --- /dev/null +++ b/art/23-01-2021.md @@ -0,0 +1,5 @@ +# Kaguya dancing + +A still image from the movie The Tale of Princess Kaguya. + +![Kaguyra Dancing](kaguya.jpg) \ No newline at end of file diff --git a/art/index.md b/art/index.md new file mode 100644 index 0000000..f15db2d --- /dev/null +++ b/art/index.md @@ -0,0 +1,3 @@ +# Art + +In this section I list art pieces that I enjoy. Creating pages in this art section is very simple: just add a new file and the generator will create them instantly! \ No newline at end of file diff --git a/blog.html b/blog.html deleted file mode 100644 index bd6202c..0000000 --- a/blog.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - Blog | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - -

Blog

-

I post my artist progress here!

- -
-
- - - \ No newline at end of file diff --git a/blog/01-01-2021.md b/blog/01-01-2021.md new file mode 100644 index 0000000..32d3b4d --- /dev/null +++ b/blog/01-01-2021.md @@ -0,0 +1,3 @@ +# Happy new year! + +Happy new year to all of you! \ No newline at end of file diff --git a/blog/24-12-2021.md b/blog/24-12-2021.md new file mode 100644 index 0000000..a4e75d4 --- /dev/null +++ b/blog/24-12-2021.md @@ -0,0 +1,3 @@ +# Merry Christmas! + +Merry Christmas to all of you! I had a lovely dinner and got super socks from my partner! \ No newline at end of file diff --git a/blog/index.md b/blog/index.md new file mode 100644 index 0000000..e4915c9 --- /dev/null +++ b/blog/index.md @@ -0,0 +1,3 @@ +# Blog + +I post my artist progress here! \ No newline at end of file diff --git a/feed.xml b/feed.xml deleted file mode 100644 index bd08f36..0000000 --- a/feed.xml +++ /dev/null @@ -1,172 +0,0 @@ - - - -Chīsai | Chīsai, a small and ready to use micro-site generator. -https://thomasorus.github.io/Chisai/ -Chīsai — Chīsai, a small and ready to use micro-site generator. -2023-02-13 - - https://thomasorus.github.io/Chisai/assets/rss.png - Chīsai — Chīsai, a small and ready to use micro-site generator. -https://thomasorus.github.io/Chisai/ - - - Art - https://thomasorus.github.io/Chisai/art.html - https://thomasorus.github.io/Chisai/art.html - 2023-02-14 00:31:52 - -Art -

In this section I list art pieces that I enjoy. Creating pages in this art section is very simple: just add a new file and the generator will create them instantly!

-]]> -
-
- Blog - https://thomasorus.github.io/Chisai/blog.html - https://thomasorus.github.io/Chisai/blog.html - 2023-02-14 00:31:52 - -Blog -

I post my artist progress here!

-]]> -
-
- How to name files - https://thomasorus.github.io/Chisai/naming-demo.html - https://thomasorus.github.io/Chisai/naming-demo.html - 2023-02-14 00:31:52 - -How to name files -

This page lists the different ways of naming files.

-]]> -
-
- No date - https://thomasorus.github.io/Chisai/no-date.html - https://thomasorus.github.io/Chisai/no-date.html - 2023-02-14 00:31:52 - -No date -

This file has no date. It means it won't be listed using a choosen date but using the last time the file was edited.

-]]> -
-
- This is a no date test to see how things are working. - https://thomasorus.github.io/Chisai/no-date-test.html - https://thomasorus.github.io/Chisai/no-date-test.html - 2023-02-14 00:31:52 - -This is a no date test to see how things are working. -

This file has no date. It means it won't be listed using a choosen date but using the last time the file was edited.

-

This is a new test.

-]]> -
-
- Merry Christmas! - https://thomasorus.github.io/Chisai/24-12-2021.html - https://thomasorus.github.io/Chisai/24-12-2021.html - 2021-12-24 00:00:00 - -Merry Christmas! -

Merry Christmas to all of you! I had a lovely dinner and got super socks from my partner!

-]]> -
-
- Simple Date - https://thomasorus.github.io/Chisai/23-05-2021.html - https://thomasorus.github.io/Chisai/23-05-2021.html - 2021-05-23 00:00:00 - -Simple Date -

This is a post with a single date. This date will be used both for listing and for the url.

-]]> -
-
- Kaguya dancing - https://thomasorus.github.io/Chisai/23-01-2021.html - https://thomasorus.github.io/Chisai/23-01-2021.html - 2021-01-23 00:00:00 - -Kaguya dancing -

A still image from the movie The Tale of Princess Kaguya.

-

Kaguyra Dancing

-]]> -
-
- Chihiro having dinner - https://thomasorus.github.io/Chisai/12-01-2021.html - https://thomasorus.github.io/Chisai/12-01-2021.html - 2021-01-12 00:00:00 - -Chihiro having dinner -

A still image from the movie Chihiro by Studio Ghibli.

-

Chihiro having dinner

-]]> -
-
- A strange looking city - https://thomasorus.github.io/Chisai/08-01-2021.html - https://thomasorus.github.io/Chisai/08-01-2021.html - 2021-01-08 00:00:00 - -A strange looking city -

This city is by François Schuiten.

-

Strange floating city by François Schuiten

-]]> -
-
- A pensive blue man - https://thomasorus.github.io/Chisai/05-01-2021.html - https://thomasorus.github.io/Chisai/05-01-2021.html - 2021-01-05 00:00:00 - -A pensive blue man -

This is from Watchen by Dave Gibbons.

-

Blue man thinking by Dave Gibbons

-]]> -
-
- A lady laying down - https://thomasorus.github.io/Chisai/02-01-2021.html - https://thomasorus.github.io/Chisai/02-01-2021.html - 2021-01-02 00:00:00 - -A lady laying down -

This art is by Zeen Chin.

-

Art by Zeen Chin

-]]> -
-
- Date and name - https://thomasorus.github.io/Chisai/name-of-file.html - https://thomasorus.github.io/Chisai/name-of-file.html - 2021-01-02 00:00:00 - -Date and name -

This file has both a date and a name. It means it will use the indicated date for listings, and the rest of the name for the url.

-]]> -
-
- Happy new year! - https://thomasorus.github.io/Chisai/01-01-2021.html - https://thomasorus.github.io/Chisai/01-01-2021.html - 2021-01-01 00:00:00 - -Happy new year! -

Happy new year to all of you!

-]]> -
-
-
-
\ No newline at end of file diff --git a/home.md b/home.md new file mode 100644 index 0000000..468f61d --- /dev/null +++ b/home.md @@ -0,0 +1,5 @@ +# Hello! + +![Chisai logo](medias/chisai_square.png) + +This is the home page of Chīsai, a small static site generator. \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 9a7895b..0000000 --- a/index.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - Home | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - -

Hello!

-

Chisai logo

-

This is the home page of Chīsai, a small static site generator.

-

Art

See more

Blog

See more

Naming-demo

See more -
-
- - - \ No newline at end of file diff --git a/name-of-file.html b/name-of-file.html deleted file mode 100644 index 4ac70e0..0000000 --- a/name-of-file.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - Date and name | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - 02-01-2021 -

Date and name

-

This file has both a date and a name. It means it will use the indicated date for listings, and the rest of the name for the url.

- -
-
- - - \ No newline at end of file diff --git a/naming-demo.html b/naming-demo.html deleted file mode 100644 index 2a8ce54..0000000 --- a/naming-demo.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - How to name files | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - -

How to name files

-

This page lists the different ways of naming files.

- -
-
- - - \ No newline at end of file diff --git a/naming-demo/02-01-2021-name-of-file.md b/naming-demo/02-01-2021-name-of-file.md new file mode 100644 index 0000000..d3f2993 --- /dev/null +++ b/naming-demo/02-01-2021-name-of-file.md @@ -0,0 +1,3 @@ +# Date and name + +This file has both a date and a name. It means it will use the indicated date for listings, and the rest of the name for the url. \ No newline at end of file diff --git a/naming-demo/23-05-2021.md b/naming-demo/23-05-2021.md new file mode 100644 index 0000000..09534ee --- /dev/null +++ b/naming-demo/23-05-2021.md @@ -0,0 +1,3 @@ +# Simple Date + +This is a post with a single date. This date will be used both for listing and for the url. \ No newline at end of file diff --git a/naming-demo/index.md b/naming-demo/index.md new file mode 100644 index 0000000..f62e060 --- /dev/null +++ b/naming-demo/index.md @@ -0,0 +1,3 @@ +# How to name files + +This page lists the different ways of naming files. diff --git a/naming-demo/no-date-test.md b/naming-demo/no-date-test.md new file mode 100644 index 0000000..f3c59c7 --- /dev/null +++ b/naming-demo/no-date-test.md @@ -0,0 +1,5 @@ +# This is a no date test to see how things are working. + +This file has no date. It means it won't be listed using a choosen date but using the last time the file was edited. + +This is a new test. \ No newline at end of file diff --git a/naming-demo/no-date.md b/naming-demo/no-date.md new file mode 100644 index 0000000..2b618d1 --- /dev/null +++ b/naming-demo/no-date.md @@ -0,0 +1,3 @@ +# No date + +This file has no date. It means it won't be listed using a choosen date but using the last time the file was edited. \ No newline at end of file diff --git a/no-date-test.html b/no-date-test.html deleted file mode 100644 index f38e493..0000000 --- a/no-date-test.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - This is a no date test to see how things are working. | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - 14-02-2023 00:31:52 -

This is a no date test to see how things are working.

-

This file has no date. It means it won't be listed using a choosen date but using the last time the file was edited.

-

This is a new test.

- -
-
- - - \ No newline at end of file diff --git a/no-date.html b/no-date.html deleted file mode 100644 index 1017edd..0000000 --- a/no-date.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - No date | Chīsai - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Chīsai -
-
-
- - - 14-02-2023 00:31:52 -

No date

-

This file has no date. It means it won't be listed using a choosen date but using the last time the file was edited.

- -
-
- - - \ No newline at end of file diff --git a/partials/item.xml b/partials/item.xml new file mode 100644 index 0000000..5daaebf --- /dev/null +++ b/partials/item.xml @@ -0,0 +1,9 @@ + + rssItemTitle + rssItemUrl + rssItemUrl + rssItemDate + + + + \ No newline at end of file diff --git a/partials/main.html b/partials/main.html new file mode 100644 index 0000000..618ab9a --- /dev/null +++ b/partials/main.html @@ -0,0 +1,56 @@ + + + + + + + page_title | name_of_site + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ name_of_site +
+
+
+ page_navigation + page_date + page_body +
+
+ + + \ No newline at end of file diff --git a/partials/nav.html b/partials/nav.html new file mode 100644 index 0000000..705f657 --- /dev/null +++ b/partials/nav.html @@ -0,0 +1,3 @@ + diff --git a/partials/rss.xml b/partials/rss.xml new file mode 100644 index 0000000..f863bd6 --- /dev/null +++ b/partials/rss.xml @@ -0,0 +1,15 @@ + + + +name_of_site | site_meta_description +build_url +name_of_site — site_meta_description +date_build + + build_urlassets/rss.png + name_of_site — site_meta_description +build_url + +rss_content + + \ No newline at end of file diff --git a/src/build.py b/src/build.py new file mode 100644 index 0000000..eca18ec --- /dev/null +++ b/src/build.py @@ -0,0 +1,440 @@ + +import argparse +import glob +import os +import platform +import re +import shutil +import subprocess +import sys +from datetime import datetime +import pathlib + +# Default encoding +encoding = 'utf-8' + +# Import local files +mistune = __import__('mistune') +config = __import__('config') + + +# Activating the renderer +renderer = mistune.Renderer() +markdown = mistune.Markdown(renderer=renderer) + + +# Checking for dev command +parser = argparse.ArgumentParser() +parser.add_argument( + "-p", "--prod", help="Prints the supplied argument.", action="store_true") +args = parser.parse_args() + +# Declaring build url that will be used in several parts of the app +build_url = config.relative_build_url +if args.prod: + build_url = config.absolute_build_url + + +# Generates html files in the site folder, using the entries and the template. +def generate_html_pages(site_folder, entries, template, sub_pages_list, template_nav): + for entry in entries: + page_template = template.replace('page_title', entry['title']) + + # Checking if the page is root of a folder + if entry["file"] == "index": + + # If root page + # Concatenate the content of the index page with the listing of sub-pages + # Remove "page_date" from template + # Replaces "page_body" with the page content in the template + entry["pageContent"] += sub_pages_list + page_template = page_template.replace('page_date', "") + page_template = page_template.replace( + 'page_body', entry['pageContent']) + else: + # If content page + # Replaces "page_body" with the page content in the template + # Replaces "page_date" with the page date in the template + page_template = page_template.replace( + 'page_body', entry['pageContent']) + page_template = page_template.replace( + 'page_date', "%s" % (entry['date'])) + + # Creating navigation + url_link = build_url + url_text = entry['parent_text'] + + if not entry["parent_url"]: + # If index page, return to the home + url_link = build_url + else: + # If content page, return to parent page + url_link = build_url + entry['parent_url'] + + nav_template = open(template_nav, 'r').read() + nav_html = nav_template.replace("link_url", url_link) + nav_html = nav_html.replace("text_url", url_text) + nav_html = nav_html.replace("go_back", config.go_back) + page_template = page_template.replace('page_navigation', nav_html) + + # Replaces all occurrences of build_url in the template files (assets, urls, etc) + page_template = page_template.replace('build_url', build_url) + + page_template = page_template.replace( + 'name_of_site', config.name_of_site) + page_template = page_template.replace( + 'site_meta_description', config.site_meta_description) + page_template = page_template.replace( + 'twitter_name', config.twitter_name) + + # Checking if content folder exists + folderExists = os.path.exists(site_folder+entry['folder']) + # If not, create it + if config.flat_build == False: + if not folderExists: + os.mkdir(site_folder+entry['folder']) + + # Write the HTML file + slug_file = site_folder + entry['slug'] + with open(slug_file, 'w', encoding=encoding) as fobj: + fobj.write(page_template) + + print("All pages created!") + + +# Get title by parsing and cleaning the first line of the markdown file +def get_entry_title(page): + pageContent = open(page, 'r') + textContent = pageContent.read() + textContent = textContent.splitlines() + textContent = textContent[0] + textContent = textContent.replace('# ', '') + + return textContent + + +# Get the slug from the markdown file name +def get_entry_slug(page): + slug = page.split("/")[-1] + slug = re.sub('\.md$', '', slug) + if slug: + return slug + else: + return '' + + +def style_iframes(page): + regex = r"<\/iframe>" + matches = re.finditer(regex, page, re.MULTILINE) + + for matchNum, match in enumerate(matches, start=1): + match = match.group() + iframe = "
%s
" % match + page = page.replace(match, iframe) + return page + +# Checks for local images links in markdown and add the build_url and medias_folder url + + +def fix_images_urls(page): + regex = r"\!\[.*\]\((.*)\)" + matches = re.finditer(regex, page, re.MULTILINE) + + for matchNum, match in enumerate(matches, start=1): + for groupNum in range(0, len(match.groups())): + captured_group = match.group(groupNum + 1) + if captured_group[:4] != "http": + full_url = build_url + config.medias_folder + captured_group + page = page.replace(captured_group, full_url) + return page + + +def fix_amp(page): + page = page.replace(" & ", " & ") + return page + +def fix_wiki_links(page, path): + regex = r"\[\[([\s\S]+?\|?[\s\S]+?)\]\]" + matches = re.finditer(regex, page, re.MULTILINE) + for matchNum, match in enumerate(matches, start=1): + for groupNum in range(0, len(match.groups())): + captured_group = match.group(groupNum + 1) + link_elem = captured_group.split("|") + if len(link_elem) > 1: + full_url = "" + link_elem[0].strip() + "" + else: + if config.flat_build: + full_url = "" + link_elem[0].strip() + "" + else: + full_url = "" + link_elem[0].strip() + "" + page = page.replace(match[0], full_url) + return page + +# From the list of files, creates the main array of entries that will be processed later + + +def create_entries(pages): + fullContent = [] + for page in pages: + tempPage = {} + + # Process the page with dedicated functions + path = clean_path(page) + title = get_entry_title(page) + + markdown_text = open(page, 'r').read() + markdown_text = style_iframes(markdown_text) + markdown_text = fix_images_urls(markdown_text) + markdown_text = fix_wiki_links(markdown_text, path["folder"]) + pageContent = markdown(markdown_text) + + # Create the page object with all the informations we need + tempPage['slug'] = path["slug"] + tempPage['file'] = path['file'] + tempPage['folder'] = path["folder"] + tempPage['parent_url'] = path['parent_url'] + tempPage['parent_text'] = path['parent_text'] + tempPage['date'] = path['date'] + tempPage['iso_date'] = path['iso_date'] + tempPage['title'] = fix_amp(title) + tempPage['pageContent'] = pageContent + + fullContent.append(tempPage) + + return fullContent + + +# Copy assets to production folder +def move_files(site_folder, path): + assets = os.listdir(path) + if assets: + for asset in assets: + asset = os.path.join(path, asset) + if os.path.isfile(asset): + shutil.copy(asset, site_folder+path) + else: + print("No assets found!") + + +# Transforms the file locations to an array of strings +def clean_path(path): + path_clean = re.sub('\.md$', '', path) + items = [] + if(platform.system() == 'Windows'): + items = path_clean.split('\\') + else: + items = path_clean.split('/') + + path_items = { + "slug" : None, + "date" : None, + "folder": items[0], + "file": items[1] + } + + # xx-xx-xxxx = xx-xx-xxxx.html + # xx-xx-xxxx-string or string = string.html or /folder/string.html + + # Checking if the file has a date + regex = r"(?:[0-9]{2}-){2}[0-9]{4}" + match = re.match(regex, path_items["file"]) + has_date = False + + if match: + has_date = True + + if has_date: + if match[0] != path_items["file"]: + path_items["date"] = match[0] + path_items["slug"] = path_items["file"].replace(match[0] + "-", "") + ".html" + else: + path_items["date"] = match[0] + path_items["slug"] = path_items["file"] + ".html" + + # Converts the EU date to US date to allow page sorting + if config.date_format == "EU": + path_items["iso_date"] = str(datetime.strptime(path_items["date"], '%d-%m-%Y')) + if config.date_format == "ISO": + path_items["iso_date"] = str(datetime.strptime(path_items["date"], '%Y-%m-%d')) + else: + path_items["slug"] = path_items["file"] + ".html" + + last_edit = str(subprocess.check_output('git log -1 --format="%ci" ' + path, shell=True)).replace("b'", "").replace("\\n'", '') + last_edit_iso = datetime.strptime(last_edit[:-6], "%Y-%m-%d %H:%M:%S") + + if config.date_format == "EU": + path_items["date"] = str(last_edit_iso.strftime("%d-%m-%Y %H:%M:%S")) + print(path_items["date"]) + else: + path_items["date"] = str(last_edit_iso) + + path_items["iso_date"] = str(last_edit_iso) + + if config.flat_build == False: + path_items["slug"] = path_items["folder"] + "/" + path_items["slug"] + + if path_items["file"] == "index": + path_items["parent_url"] = "" + path_items["parent_text"] = config.home_name + if config.flat_build: + path_items["slug"] = path_items["folder"] + ".html" + else: + if config.flat_build: + path_items["parent_url"] = path_items["folder"] + ".html" + path_items["parent_text"] = path_items["folder"].replace("-", " ").capitalize() + else: + path_items["parent_url"] = path_items["folder"] + path_items["parent_text"] = path_items["folder"].replace("-", " ").capitalize() + + + return path_items + + +# Generate the list of sub pages for each section +def generate_sub_pages(entries, num, folder, title): + + # Sort entries by date using the iso_date format + entries.sort(key=lambda x: x["iso_date"], reverse=True) + + # Take n number of entries (5 for the home, all for the sub-section pages) + selected_entries = entries[:num] + + # Create the list + sub_page_list = "" + + # If a title is necessary, use the folder name + if title: + title = "

%s

" % folder.capitalize() + sub_page_list = title + sub_page_list + if config.flat_build: + sub_page_link = build_url + folder + ".html" + else: + sub_page_link = build_url + folder + sub_page_link_html = "" % sub_page_link + config.see_all + "" + sub_page_list += sub_page_link_html + + return sub_page_list + + +# Creates the home page using home.md +def create_home_page(template, site_folder): + + # Read the file and add "content_list" as a future replacement point for sub page listing + html = markdown(open("home.md", "r").read()) + "content_list" + + # Replace template strings with content + template = template.replace('page_title', config.home_name) + template = template.replace('page_body', html) + template = template.replace('build_url', build_url) + template = template.replace('page_navigation', "") + + return template + + +# Create RSS Feed +def create_rss_feed(rss_entries, rss_template, rss_item_template, site_folder): + template = open(rss_template, 'r').read() + itemTemplate = open(rss_item_template, 'r').read() + rss_entries.sort(key=lambda x: x["iso_date"], reverse=True) + + rss_items = "" + for rss_entry in rss_entries: + entry_template = itemTemplate + entry_template = entry_template.replace( + 'rssItemTitle', rss_entry["title"]) + entry_template = entry_template.replace('rssItemUrl', build_url + + rss_entry["slug"]) + entry_template = entry_template.replace( + 'rssItemDate', rss_entry["iso_date"]) + entry_template = entry_template.replace( + 'rssItemContent', rss_entry["pageContent"]) + rss_items += entry_template + + template = template.replace('name_of_site', config.name_of_site) + template = template.replace('site_meta_description', + config.site_meta_description) + template = template.replace('build_url', build_url) + template = template.replace('date_build', str( + datetime.now().date())) + template = template.replace('rss_content', rss_items) + + slug_file = site_folder + "feed.xml" + with open(slug_file, 'w', encoding=encoding) as fobj: + fobj.write(template) + + return + + +def generate_website(): + print('Welcome to the builder!') + + # If build folder exists delete it + if os.path.exists(config.build_folder): + shutil.rmtree(config.build_folder) + + # Make new folders + os.makedirs(config.build_folder + config.assets_folder) + os.makedirs(config.build_folder + config.medias_folder) + + # Get main html template + template = open(config.template_file, 'r').read() + + # Create home page + home_page = create_home_page(template, config.build_folder) + + rss_entries = [] + + for folder in config.content_folder: + pages = glob.glob(folder + '**/*.md', recursive=True) + entries = create_entries(pages) + sub_pages_list = generate_sub_pages( + entries, len(entries), folder, False) + + # For each section, create a short listing of sub pages and add it to the home page + home_pageSubList = generate_sub_pages(entries, 5, folder, True) + home_page = home_page.replace('content_list', home_pageSubList + + "content_list") + home_page = home_page.replace('page_date', "") + + generate_html_pages(config.build_folder, entries, template, + sub_pages_list, config.template_nav) + + for entry in entries: + rss_entries.append(entry) + + # Move the assets + move_files(config.build_folder, config.assets_folder) + move_files(config.build_folder, config.medias_folder) + + # Once all sections have been processed, finish the home page + # Removes the "content_list" in the partial + home_page = home_page.replace('content_list', "") + home_page = home_page.replace('name_of_site', config.name_of_site) + home_page = home_page.replace('site_meta_description', + config.site_meta_description) + home_page = home_page.replace( + 'twitter_name', config.twitter_name) + + slug_file = config.build_folder + "index.html" + with open(slug_file, 'w', encoding=encoding) as fobj: + fobj.write(home_page) + + # Create RSS File + create_rss_feed(rss_entries, config.rss_template, + config.rss_item_template, config.build_folder) + + +# Triggers the website build +generate_website() diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..310e8f6 --- /dev/null +++ b/src/config.py @@ -0,0 +1,18 @@ +content_folder = ['art', 'blog', 'naming-demo'] +relative_build_url = "/" +absolute_build_url = "https://thomasorus.github.io/Chisai/" +build_folder = "build/" +assets_folder = "assets/" +medias_folder = "medias/" +template_file = "partials/main.html" +template_nav = "partials/nav.html" +rss_template = "partials/rss.xml" +rss_item_template = "partials/item.xml" +name_of_site = "Chīsai" +site_meta_description = "Chīsai, a small and ready to use micro-site generator." +twitter_name = "@thomasorus" +home_name = "Home" +see_all = "See more" +go_back = "Go back to" +date_format = "EU" # Choose between EU day-month-year or ISO year-mont-day +flat_build = True # True or False, if True, all files will be written in the root of the website \ No newline at end of file diff --git a/src/mistune.py b/src/mistune.py new file mode 100644 index 0000000..f06f56d --- /dev/null +++ b/src/mistune.py @@ -0,0 +1,1154 @@ +# coding: utf-8 +""" + mistune + ~~~~~~~ + The fastest markdown parser in pure Python with renderer feature. + :copyright: (c) 2014 - 2018 by Hsiaoming Yang. +""" + +import re +import inspect + +__version__ = '0.8.4' +__author__ = 'Hsiaoming Yang ' +__all__ = [ + 'BlockGrammar', 'BlockLexer', + 'InlineGrammar', 'InlineLexer', + 'Renderer', 'Markdown', + 'markdown', 'escape', +] + + +_key_pattern = re.compile(r'\s+') +_nonalpha_pattern = re.compile(r'\W') +_escape_pattern = re.compile(r'&(?!#?\w+;)') +_newline_pattern = re.compile(r'\r\n|\r') +_block_quote_leading_pattern = re.compile(r'^ *> ?', flags=re.M) +_block_code_leading_pattern = re.compile(r'^ {4}', re.M) +_inline_tags = [ + 'a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data', + 'time', 'code', 'var', 'samp', 'kbd', 'sub', 'sup', 'i', 'b', 'u', 'mark', + 'ruby', 'rt', 'rp', 'bdi', 'bdo', 'span', 'br', 'wbr', 'ins', 'del', + 'img', 'font', +] +_pre_tags = ['pre', 'script', 'style'] +_valid_end = r'(?!:/|[^\w\s@]*@)\b' +_valid_attr = r'''\s*[a-zA-Z\-](?:\s*\=\s*(?:"[^"]*"|'[^']*'|[^\s'">]+))?''' +_block_tag = r'(?!(?:%s)\b)\w+%s' % ('|'.join(_inline_tags), _valid_end) +_scheme_blacklist = ('javascript:', 'vbscript:') + + +def _pure_pattern(regex): + pattern = regex.pattern + if pattern.startswith('^'): + pattern = pattern[1:] + return pattern + + +def _keyify(key): + key = escape(key.lower(), quote=True) + return _key_pattern.sub(' ', key) + + +def escape(text, quote=False, smart_amp=True): + """Replace special characters "&", "<" and ">" to HTML-safe sequences. + The original cgi.escape will always escape "&", but you can control + this one for a smart escape amp. + :param quote: if set to True, " and ' will be escaped. + :param smart_amp: if set to False, & will always be escaped. + """ + if smart_amp: + text = _escape_pattern.sub('&', text) + else: + text = text.replace('&', '&') + text = text.replace('<', '<') + text = text.replace('>', '>') + if quote: + text = text.replace('"', '"') + text = text.replace("'", ''') + return text + + +def escape_link(url): + """Remove dangerous URL schemes like javascript: and escape afterwards.""" + lower_url = url.lower().strip('\x00\x1a \n\r\t') + + for scheme in _scheme_blacklist: + if re.sub(r'[^A-Za-z0-9\/:]+', '', lower_url).startswith(scheme): + return '' + return escape(url, quote=True, smart_amp=False) + + +def preprocessing(text, tab=4): + text = _newline_pattern.sub('\n', text) + text = text.expandtabs(tab) + text = text.replace('\u2424', '\n') + pattern = re.compile(r'^ +$', re.M) + return pattern.sub('', text) + + +class BlockGrammar(object): + """Grammars for block level tokens.""" + + def_links = re.compile( + r'^ *\[([^^\]]+)\]: *' # [key]: + r']+)>?' # or link + r'(?: +["(]([^\n]+)[")])? *(?:\n+|$)' + ) + def_footnotes = re.compile( + r'^\[\^([^\]]+)\]: *(' + r'[^\n]*(?:\n+|$)' # [^key]: + r'(?: {1,}[^\n]*(?:\n+|$))*' + r')' + ) + + newline = re.compile(r'^\n+') + block_code = re.compile(r'^( {4}[^\n]+\n*)+') + fences = re.compile( + r'^ *(`{3,}|~{3,}) *([^`\s]+)? *\n' # ```lang + r'([\s\S]+?)\s*' + r'\1 *(?:\n+|$)' # ``` + ) + hrule = re.compile(r'^ {0,3}[-*_](?: *[-*_]){2,} *(?:\n+|$)') + heading = re.compile(r'^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)') + lheading = re.compile(r'^([^\n]+)\n *(=|-)+ *(?:\n+|$)') + block_quote = re.compile(r'^( *>[^\n]+(\n[^\n]+)*\n*)+') + list_block = re.compile( + r'^( *)(?=[*+-]|\d+\.)(([*+-])?(?:\d+\.)?) [\s\S]+?' + r'(?:' + r'\n+(?=\1?(?:[-*_] *){3,}(?:\n+|$))' # hrule + r'|\n+(?=%s)' # def links + r'|\n+(?=%s)' # def footnotes\ + r'|\n+(?=\1(?(3)\d+\.|[*+-]) )' # heterogeneous bullet + r'|\n{2,}' + r'(?! )' + r'(?!\1(?:[*+-]|\d+\.) )\n*' + r'|' + r'\s*$)' % ( + _pure_pattern(def_links), + _pure_pattern(def_footnotes), + ) + ) + list_item = re.compile( + r'^(( *)(?:[*+-]|\d+\.) [^\n]*' + r'(?:\n(?!\2(?:[*+-]|\d+\.) )[^\n]*)*)', + flags=re.M + ) + list_bullet = re.compile(r'^ *(?:[*+-]|\d+\.) +') + paragraph = re.compile( + r'^((?:[^\n]+\n?(?!' + r'%s|%s|%s|%s|%s|%s|%s|%s|%s' + r'))+)\n*' % ( + _pure_pattern(fences).replace(r'\1', r'\2'), + _pure_pattern(list_block).replace(r'\1', r'\3'), + _pure_pattern(hrule), + _pure_pattern(heading), + _pure_pattern(lheading), + _pure_pattern(block_quote), + _pure_pattern(def_links), + _pure_pattern(def_footnotes), + '<' + _block_tag, + ) + ) + block_html = re.compile( + r'^ *(?:%s|%s|%s) *(?:\n{2,}|\s*$)' % ( + r'', + r'<(%s)((?:%s)*?)>([\s\S]*?)<\/\1>' % (_block_tag, _valid_attr), + r'<%s(?:%s)*?\s*\/?>' % (_block_tag, _valid_attr), + ) + ) + table = re.compile( + r'^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*' + ) + nptable = re.compile( + r'^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*' + ) + text = re.compile(r'^[^\n]+') + + +class BlockLexer(object): + """Block level lexer for block grammars.""" + grammar_class = BlockGrammar + + default_rules = [ + 'newline', 'hrule', 'block_code', 'fences', 'heading', + 'nptable', 'lheading', 'block_quote', + 'list_block', 'block_html', 'def_links', + 'def_footnotes', 'table', 'paragraph', 'text' + ] + + list_rules = ( + 'newline', 'block_code', 'fences', 'lheading', 'hrule', + 'block_quote', 'list_block', 'block_html', 'text', + ) + + footnote_rules = ( + 'newline', 'block_code', 'fences', 'heading', + 'nptable', 'lheading', 'hrule', 'block_quote', + 'list_block', 'block_html', 'table', 'paragraph', 'text' + ) + + def __init__(self, rules=None, **kwargs): + self.tokens = [] + self.def_links = {} + self.def_footnotes = {} + + if not rules: + rules = self.grammar_class() + + self.rules = rules + self._max_recursive_depth = kwargs.get('max_recursive_depth', 6) + self._list_depth = 0 + self._blockquote_depth = 0 + + def __call__(self, text, rules=None): + return self.parse(text, rules) + + def parse(self, text, rules=None): + text = text.rstrip('\n') + + if not rules: + rules = self.default_rules + + def manipulate(text): + for key in rules: + rule = getattr(self.rules, key) + m = rule.match(text) + if not m: + continue + getattr(self, 'parse_%s' % key)(m) + return m + return False # pragma: no cover + + while text: + m = manipulate(text) + if m is not False: + text = text[len(m.group(0)):] + continue + if text: # pragma: no cover + raise RuntimeError('Infinite loop at: %s' % text) + return self.tokens + + def parse_newline(self, m): + length = len(m.group(0)) + if length > 1: + self.tokens.append({'type': 'newline'}) + + def parse_block_code(self, m): + # clean leading whitespace + code = _block_code_leading_pattern.sub('', m.group(0)) + self.tokens.append({ + 'type': 'code', + 'lang': None, + 'text': code, + }) + + def parse_fences(self, m): + self.tokens.append({ + 'type': 'code', + 'lang': m.group(2), + 'text': m.group(3), + }) + + def parse_heading(self, m): + self.tokens.append({ + 'type': 'heading', + 'level': len(m.group(1)), + 'text': m.group(2), + }) + + def parse_lheading(self, m): + """Parse setext heading.""" + self.tokens.append({ + 'type': 'heading', + 'level': 1 if m.group(2) == '=' else 2, + 'text': m.group(1), + }) + + def parse_hrule(self, m): + self.tokens.append({'type': 'hrule'}) + + def parse_list_block(self, m): + bull = m.group(2) + self.tokens.append({ + 'type': 'list_start', + 'ordered': '.' in bull, + }) + self._list_depth += 1 + if self._list_depth > self._max_recursive_depth: + self.tokens.append({'type': 'list_item_start'}) + self.parse_text(m) + self.tokens.append({'type': 'list_item_end'}) + else: + cap = m.group(0) + self._process_list_item(cap, bull) + self.tokens.append({'type': 'list_end'}) + self._list_depth -= 1 + + def _process_list_item(self, cap, bull): + cap = self.rules.list_item.findall(cap) + + _next = False + length = len(cap) + + for i in range(length): + item = cap[i][0] + + # remove the bullet + space = len(item) + item = self.rules.list_bullet.sub('', item) + + # outdent + if '\n ' in item: + space = space - len(item) + pattern = re.compile(r'^ {1,%d}' % space, flags=re.M) + item = pattern.sub('', item) + + # determine whether item is loose or not + loose = _next + if not loose and re.search(r'\n\n(?!\s*$)', item): + loose = True + + rest = len(item) + if i != length - 1 and rest: + _next = item[rest-1] == '\n' + if not loose: + loose = _next + + if loose: + t = 'loose_item_start' + else: + t = 'list_item_start' + + self.tokens.append({'type': t}) + # recurse + self.parse(item, self.list_rules) + self.tokens.append({'type': 'list_item_end'}) + + def parse_block_quote(self, m): + self.tokens.append({'type': 'block_quote_start'}) + self._blockquote_depth += 1 + if self._blockquote_depth > self._max_recursive_depth: + self.parse_text(m) + else: + # clean leading > + cap = _block_quote_leading_pattern.sub('', m.group(0)) + self.parse(cap) + self.tokens.append({'type': 'block_quote_end'}) + self._blockquote_depth -= 1 + + def parse_def_links(self, m): + key = _keyify(m.group(1)) + self.def_links[key] = { + 'link': m.group(2), + 'title': m.group(3), + } + + def parse_def_footnotes(self, m): + key = _keyify(m.group(1)) + if key in self.def_footnotes: + # footnote is already defined + return + + self.def_footnotes[key] = 0 + + self.tokens.append({ + 'type': 'footnote_start', + 'key': key, + }) + + text = m.group(2) + + if '\n' in text: + lines = text.split('\n') + whitespace = None + for line in lines[1:]: + space = len(line) - len(line.lstrip()) + if space and (not whitespace or space < whitespace): + whitespace = space + newlines = [lines[0]] + for line in lines[1:]: + newlines.append(line[whitespace:]) + text = '\n'.join(newlines) + + self.parse(text, self.footnote_rules) + + self.tokens.append({ + 'type': 'footnote_end', + 'key': key, + }) + + def parse_table(self, m): + item = self._process_table(m) + + cells = re.sub(r'(?: *\| *)?\n$', '', m.group(3)) + cells = cells.split('\n') + for i, v in enumerate(cells): + v = re.sub(r'^ *\| *| *\| *$', '', v) + cells[i] = re.split(r' *(?~|])') # \* \+ \! .... + inline_html = re.compile( + r'^(?:%s|%s|%s)' % ( + r'', + r'<(\w+%s)((?:%s)*?)\s*>([\s\S]*?)<\/\1>' % ( + _valid_end, _valid_attr), + r'<\w+%s(?:%s)*?\s*\/?>' % (_valid_end, _valid_attr), + ) + ) + autolink = re.compile(r'^<([^ >]+(@|:)[^ >]+)>') + link = re.compile( + r'^!?\[(' + r'(?:\[[^^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*' + r')\]\(' + r'''\s*(<)?([\s\S]*?)(?(2)>)(?:\s+['"]([\s\S]*?)['"])?\s*''' + r'\)' + ) + reflink = re.compile( + r'^!?\[(' + r'(?:\[[^^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*' + r')\]\s*\[([^^\]]*)\]' + ) + nolink = re.compile(r'^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]') + url = re.compile(r'''^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])''') + double_emphasis = re.compile( + r'^_{2}([\s\S]+?)_{2}(?!_)' # __word__ + r'|' + r'^\*{2}([\s\S]+?)\*{2}(?!\*)' # **word** + ) + emphasis = re.compile( + r'^\b_((?:__|[^_])+?)_\b' # _word_ + r'|' + r'^\*((?:\*\*|[^\*])+?)\*(?!\*)' # *word* + ) + code = re.compile(r'^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)') # `code` + linebreak = re.compile(r'^ {2,}\n(?!\s*$)') + strikethrough = re.compile(r'^~~(?=\S)([\s\S]*?\S)~~') # ~~word~~ + footnote = re.compile(r'^\[\^([^\]]+)\]') + text = re.compile(r'^[\s\S]+?(?=[\\%s' % (tag, extra, text, tag) + else: + html = m.group(0) + return self.renderer.inline_html(html) + + def output_footnote(self, m): + key = _keyify(m.group(1)) + if key not in self.footnotes: + return None + if self.footnotes[key]: + return None + self.footnote_index += 1 + self.footnotes[key] = self.footnote_index + return self.renderer.footnote_ref(key, self.footnote_index) + + def output_link(self, m): + return self._process_link(m, m.group(3), m.group(4)) + + def output_reflink(self, m): + key = _keyify(m.group(2) or m.group(1)) + if key not in self.links: + return None + ret = self.links[key] + return self._process_link(m, ret['link'], ret['title']) + + def output_nolink(self, m): + key = _keyify(m.group(1)) + if key not in self.links: + return None + ret = self.links[key] + return self._process_link(m, ret['link'], ret['title']) + + def _process_link(self, m, link, title=None): + line = m.group(0) + text = m.group(1) + if line[0] == '!': + return self.renderer.image(link, title, text) + + self._in_link = True + text = self.output(text) + self._in_link = False + return self.renderer.link(link, title, text) + + def output_double_emphasis(self, m): + text = m.group(2) or m.group(1) + text = self.output(text) + return self.renderer.double_emphasis(text) + + def output_emphasis(self, m): + text = m.group(2) or m.group(1) + text = self.output(text) + return self.renderer.emphasis(text) + + def output_code(self, m): + text = m.group(2) + return self.renderer.codespan(text) + + def output_linebreak(self, m): + return self.renderer.linebreak() + + def output_strikethrough(self, m): + text = self.output(m.group(1)) + return self.renderer.strikethrough(text) + + def output_text(self, m): + text = m.group(0) + return self.renderer.text(text) + + +class Renderer(object): + """The default HTML renderer for rendering Markdown. + """ + + def __init__(self, **kwargs): + self.options = kwargs + + def placeholder(self): + """Returns the default, empty output value for the renderer. + All renderer methods use the '+=' operator to append to this value. + Default is a string so rendering HTML can build up a result string with + the rendered Markdown. + Can be overridden by Renderer subclasses to be types like an empty + list, allowing the renderer to create a tree-like structure to + represent the document (which can then be reprocessed later into a + separate format like docx or pdf). + """ + return '' + + def block_code(self, code, lang=None): + """Rendering block level code. ``pre > code``. + :param code: text content of the code block. + :param lang: language of the given code. + """ + code = code.rstrip('\n') + if not lang: + code = escape(code, smart_amp=False) + return '
%s\n
\n' % code + code = escape(code, quote=True, smart_amp=False) + return '
%s\n
\n' % (lang, code) + + def block_quote(self, text): + """Rendering
with the given text. + :param text: text content of the blockquote. + """ + return '
%s\n
\n' % text.rstrip('\n') + + def block_html(self, html): + """Rendering block level pure html content. + :param html: text content of the html snippet. + """ + if self.options.get('skip_style') and \ + html.lower().startswith('`` ``

``. + :param text: rendered text content for the header. + :param level: a number for the header level, for example: 1. + :param raw: raw text content of the header. + """ + return '%s\n' % (level, text, level) + + def hrule(self): + """Rendering method for ``
`` tag.""" + if self.options.get('use_xhtml'): + return '
\n' + return '
\n' + + def list(self, body, ordered=True): + """Rendering list tags like ``
    `` and ``
      ``. + :param body: body contents of the list. + :param ordered: whether this list is ordered or not. + """ + tag = 'ul' + if ordered: + tag = 'ol' + return '<%s>\n%s\n' % (tag, body, tag) + + def list_item(self, text): + """Rendering list item snippet. Like ``
    1. ``.""" + return '
    2. %s
    3. \n' % text + + def paragraph(self, text): + """Rendering paragraph tags. Like ``

      ``.""" + return '

      %s

      \n' % text.strip(' ') + + def table(self, header, body): + """Rendering table element. Wrap header and body in it. + :param header: header part of the table. + :param body: body part of the table. + """ + return ( + '\n%s\n' + '\n%s\n
      \n' + ) % (header, body) + + def table_row(self, content): + """Rendering a table row. Like ````. + :param content: content of current table row. + """ + return '\n%s\n' % content + + def table_cell(self, content, **flags): + """Rendering a table cell. Like ```` ````. + :param content: content of current table cell. + :param header: whether this is header or not. + :param align: align of current table cell. + """ + if flags['header']: + tag = 'th' + else: + tag = 'td' + align = flags['align'] + if not align: + return '<%s>%s\n' % (tag, content, tag) + return '<%s style="text-align:%s">%s\n' % ( + tag, align, content, tag + ) + + def double_emphasis(self, text): + """Rendering **strong** text. + :param text: text content for emphasis. + """ + return '%s' % text + + def emphasis(self, text): + """Rendering *emphasis* text. + :param text: text content for emphasis. + """ + return '%s' % text + + def codespan(self, text): + """Rendering inline `code` text. + :param text: text content for inline code. + """ + text = escape(text.rstrip(), smart_amp=False) + return '%s' % text + + def linebreak(self): + """Rendering line break like ``
      ``.""" + if self.options.get('use_xhtml'): + return '
      \n' + return '
      \n' + + def strikethrough(self, text): + """Rendering ~~strikethrough~~ text. + :param text: text content for strikethrough. + """ + return '%s' % text + + def text(self, text): + """Rendering unformatted text. + :param text: text content. + """ + if self.options.get('parse_block_html'): + return text + return escape(text) + + def escape(self, text): + """Rendering escape sequence. + :param text: text content. + """ + return escape(text) + + def autolink(self, link, is_email=False): + """Rendering a given link or email address. + :param link: link content or email address. + :param is_email: whether this is an email or not. + """ + text = link = escape_link(link) + if is_email: + link = 'mailto:%s' % link + return '%s' % (link, text) + + def link(self, link, title, text): + """Rendering a given link with content and title. + :param link: href link for ```` tag. + :param title: title content for `title` attribute. + :param text: text content for description. + """ + link = escape_link(link) + if not title: + return '%s' % (link, text) + title = escape(title, quote=True) + return '%s' % (link, title, text) + + def image(self, src, title, text): + """Rendering a image with title and text. + :param src: source link of the image. + :param title: title text of the image. + :param text: alt text of the image. + """ + src = escape_link(src) + text = escape(text, quote=True) + if title: + title = escape(title, quote=True) + html = '%s' % html + return '%s>' % html + + def inline_html(self, html): + """Rendering span level pure html content. + :param html: text content of the html snippet. + """ + if self.options.get('escape'): + return escape(html) + return html + + def newline(self): + """Rendering newline element.""" + return '' + + def footnote_ref(self, key, index): + """Rendering the ref anchor of a footnote. + :param key: identity key for the footnote. + :param index: the index count of current footnote. + """ + html = ( + '' + '%d' + ) % (escape(key), escape(key), index) + return html + + def footnote_item(self, key, text): + """Rendering a footnote item. + :param key: identity key for the footnote. + :param text: text content of the footnote. + """ + back = ( + '↩' + ) % escape(key) + text = text.rstrip() + if text.endswith('

      '): + text = re.sub(r'<\/p>$', r'%s

      ' % back, text) + else: + text = '%s

      %s

      ' % (text, back) + html = '
    4. %s
    5. \n' % (escape(key), text) + return html + + def footnotes(self, text): + """Wrapper for all footnotes. + :param text: contents of all footnotes. + """ + html = '
      \n%s
        %s
      \n
      \n' + return html % (self.hrule(), text) + + +class Markdown(object): + """The Markdown parser. + :param renderer: An instance of ``Renderer``. + :param inline: An inline lexer class or instance. + :param block: A block lexer class or instance. + """ + def __init__(self, renderer=None, inline=None, block=None, **kwargs): + if not renderer: + renderer = Renderer(**kwargs) + else: + kwargs.update(renderer.options) + + self.renderer = renderer + + if inline and inspect.isclass(inline): + inline = inline(renderer, **kwargs) + if block and inspect.isclass(block): + block = block(**kwargs) + + if inline: + self.inline = inline + else: + self.inline = InlineLexer(renderer, **kwargs) + + self.block = block or BlockLexer(BlockGrammar()) + self.footnotes = [] + self.tokens = [] + + # detect if it should parse text in block html + self._parse_block_html = kwargs.get('parse_block_html') + + def __call__(self, text): + return self.parse(text) + + def render(self, text): + """Render the Markdown text. + :param text: markdown formatted text content. + """ + return self.parse(text) + + def parse(self, text): + out = self.output(preprocessing(text)) + + keys = self.block.def_footnotes + + # reset block + self.block.def_links = {} + self.block.def_footnotes = {} + + # reset inline + self.inline.links = {} + self.inline.footnotes = {} + + if not self.footnotes: + return out + + footnotes = filter(lambda o: keys.get(o['key']), self.footnotes) + self.footnotes = sorted( + footnotes, key=lambda o: keys.get(o['key']), reverse=True + ) + + body = self.renderer.placeholder() + while self.footnotes: + note = self.footnotes.pop() + body += self.renderer.footnote_item( + note['key'], note['text'] + ) + + out += self.renderer.footnotes(body) + return out + + def pop(self): + if not self.tokens: + return None + self.token = self.tokens.pop() + return self.token + + def peek(self): + if self.tokens: + return self.tokens[-1] + return None # pragma: no cover + + def output(self, text, rules=None): + self.tokens = self.block(text, rules) + self.tokens.reverse() + + self.inline.setup(self.block.def_links, self.block.def_footnotes) + + out = self.renderer.placeholder() + while self.pop(): + out += self.tok() + return out + + def tok(self): + t = self.token['type'] + + # sepcial cases + if t.endswith('_start'): + t = t[:-6] + + return getattr(self, 'output_%s' % t)() + + def tok_text(self): + text = self.token['text'] + while self.peek()['type'] == 'text': + text += '\n' + self.pop()['text'] + return self.inline(text) + + def output_newline(self): + return self.renderer.newline() + + def output_hrule(self): + return self.renderer.hrule() + + def output_heading(self): + return self.renderer.header( + self.inline(self.token['text']), + self.token['level'], + self.token['text'], + ) + + def output_code(self): + return self.renderer.block_code( + self.token['text'], self.token['lang'] + ) + + def output_table(self): + aligns = self.token['align'] + aligns_length = len(aligns) + cell = self.renderer.placeholder() + + # header part + header = self.renderer.placeholder() + for i, value in enumerate(self.token['header']): + align = aligns[i] if i < aligns_length else None + flags = {'header': True, 'align': align} + cell += self.renderer.table_cell(self.inline(value), **flags) + + header += self.renderer.table_row(cell) + + # body part + body = self.renderer.placeholder() + for i, row in enumerate(self.token['cells']): + cell = self.renderer.placeholder() + for j, value in enumerate(row): + align = aligns[j] if j < aligns_length else None + flags = {'header': False, 'align': align} + cell += self.renderer.table_cell(self.inline(value), **flags) + body += self.renderer.table_row(cell) + + return self.renderer.table(header, body) + + def output_block_quote(self): + body = self.renderer.placeholder() + while self.pop()['type'] != 'block_quote_end': + body += self.tok() + return self.renderer.block_quote(body) + + def output_list(self): + ordered = self.token['ordered'] + body = self.renderer.placeholder() + while self.pop()['type'] != 'list_end': + body += self.tok() + return self.renderer.list(body, ordered) + + def output_list_item(self): + body = self.renderer.placeholder() + while self.pop()['type'] != 'list_item_end': + if self.token['type'] == 'text': + body += self.tok_text() + else: + body += self.tok() + + return self.renderer.list_item(body) + + def output_loose_item(self): + body = self.renderer.placeholder() + while self.pop()['type'] != 'list_item_end': + body += self.tok() + return self.renderer.list_item(body) + + def output_footnote(self): + self.inline._in_footnote = True + body = self.renderer.placeholder() + key = self.token['key'] + while self.pop()['type'] != 'footnote_end': + body += self.tok() + self.footnotes.append({'key': key, 'text': body}) + self.inline._in_footnote = False + return self.renderer.placeholder() + + def output_close_html(self): + text = self.token['text'] + return self.renderer.block_html(text) + + def output_open_html(self): + text = self.token['text'] + tag = self.token['tag'] + if self._parse_block_html and tag not in _pre_tags: + text = self.inline(text, rules=self.inline.inline_html_rules) + extra = self.token.get('extra') or '' + html = '<%s%s>%s' % (tag, extra, text, tag) + return self.renderer.block_html(html) + + def output_paragraph(self): + return self.renderer.paragraph(self.inline(self.token['text'])) + + def output_text(self): + return self.renderer.paragraph(self.tok_text()) + + +def markdown(text, escape=True, **kwargs): + """Render markdown formatted text to html. + :param text: markdown formatted text content. + :param escape: if set to False, all html tags will not be escaped. + :param use_xhtml: output with xhtml tags. + :param hard_wrap: if set to True, it will use the GFM line breaks feature. + :param parse_block_html: parse text only in block level html. + :param parse_inline_html: parse text only in inline level html. + """ + return Markdown(escape=escape, **kwargs)(text) \ No newline at end of file