├── fonts └── .gitkeep ├── html ├── title.html ├── footer.html ├── header.html ├── script_syntax_highlighting.html ├── script_giscus.html └── head.html ├── skunk-html.slnx ├── assets ├── avatar.jpg ├── favicon-96x96.png └── apple-touch-icon.png ├── .gitattributes ├── markdown-blog ├── images │ ├── eye.png │ ├── skunk-final.png │ ├── 4-enable-pages.png │ ├── 5-run-workflow.png │ ├── skunk-drawing.jpg │ ├── 2-name-your-repo.png │ ├── first-blog-post.png │ ├── 1-create-a-new-fork.png │ ├── 3-enable-workflows.png │ └── 5-re-run-if-needed.png ├── index.md ├── links.md ├── 2024-12-26.md ├── 2024-12-27.md └── 2024-12-25.md ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── skunk-html.fsproj ├── LICENSE ├── css ├── tweaks.css └── styles.css ├── Program.fs ├── SkunkUtils.fs ├── README.md ├── SkunkHtml.fs ├── .gitignore └── scripts └── microlight.js /fonts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /html/title.html: -------------------------------------------------------------------------------- 1 | SkunkHTML -------------------------------------------------------------------------------- /skunk-html.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/assets/avatar.jpg -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-vendored 2 | *.html linguist-vendored 3 | *.js linguist-vendored -------------------------------------------------------------------------------- /assets/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/assets/favicon-96x96.png -------------------------------------------------------------------------------- /assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /markdown-blog/images/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/markdown-blog/images/eye.png -------------------------------------------------------------------------------- /markdown-blog/images/skunk-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/markdown-blog/images/skunk-final.png -------------------------------------------------------------------------------- /markdown-blog/images/4-enable-pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/markdown-blog/images/4-enable-pages.png -------------------------------------------------------------------------------- /markdown-blog/images/5-run-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/markdown-blog/images/5-run-workflow.png -------------------------------------------------------------------------------- /markdown-blog/images/skunk-drawing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/markdown-blog/images/skunk-drawing.jpg -------------------------------------------------------------------------------- /markdown-blog/images/2-name-your-repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/markdown-blog/images/2-name-your-repo.png -------------------------------------------------------------------------------- /markdown-blog/images/first-blog-post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/markdown-blog/images/first-blog-post.png -------------------------------------------------------------------------------- /markdown-blog/images/1-create-a-new-fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/markdown-blog/images/1-create-a-new-fork.png -------------------------------------------------------------------------------- /markdown-blog/images/3-enable-workflows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/markdown-blog/images/3-enable-workflows.png -------------------------------------------------------------------------------- /markdown-blog/images/5-re-run-if-needed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mg0x7BE/skunk-html/HEAD/markdown-blog/images/5-re-run-if-needed.png -------------------------------------------------------------------------------- /html/footer.html: -------------------------------------------------------------------------------- 1 | Generated with SkunkHTML: Markdown blog on GitHub Pages 2 | -------------------------------------------------------------------------------- /html/header.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /html/script_syntax_highlighting.html: -------------------------------------------------------------------------------- 1 | // Syntax highlighting with microlight.js 2 | 3 | document.addEventListener("DOMContentLoaded", function() { 4 | // All elements with "language-" class prefix: 5 | var nodes = document.querySelectorAll("pre code[class^='language-']"); 6 | microlight.reset(nodes); 7 | }); -------------------------------------------------------------------------------- /markdown-blog/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to SkunkHTML! 2 | 3 | This is an ultra-simple blog generator built on Markdown files. Just fork this repository, and GitHub Actions will automatically create your blog. To add a new post, simply create another Markdown file with a date in its filename 🦨 — Markdown in, GitHub Pages out! No coding! 4 | 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Documentation: 2 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "github-actions" 7 | directory: "/.github/workflows" 8 | schedule: 9 | interval: "weekly" 10 | 11 | - package-ecosystem: "nuget" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /markdown-blog/links.md: -------------------------------------------------------------------------------- 1 | # Links 2 | 3 | Below are the most important links related to SkunkHTML: 4 | 5 | * SkunkHTML repository on GitHub 6 | [https://github.com/mg0x7BE/skunk-html](https://github.com/mg0x7BE/skunk-html) 7 | * Discussions 8 | [https://github.com/mg0x7BE/skunk-html/discussions](https://github.com/mg0x7BE/skunk-html/discussions) 9 | * Issues 10 | [https://github.com/mg0x7BE/skunk-html/issues](https://github.com/mg0x7BE/skunk-html/issues) -------------------------------------------------------------------------------- /skunk-html.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | skunk_html 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /html/script_giscus.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /html/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title.html content}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build_and_deploy: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | pages: write 15 | id-token: write 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v6 19 | 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v5 22 | with: 23 | dotnet-version: 9 24 | 25 | - name: Build project 26 | run: dotnet build skunk-html.fsproj 27 | 28 | - name: Run F# code to generate HTML 29 | run: dotnet run --project skunk-html.fsproj 30 | 31 | - name: Upload artifact 32 | uses: actions/upload-pages-artifact@v4 33 | with: 34 | path: "./skunk-html-output" 35 | 36 | - name: Deploy to GitHub Pages 37 | id: deployment 38 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /css/tweaks.css: -------------------------------------------------------------------------------- 1 | nav a img { 2 | 3 | border-style: outset; 4 | border-color: var(--color-link); 5 | border-width: 1px; 6 | box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3); 7 | } 8 | 9 | 10 | 11 | blockquote, 12 | blockquote p { 13 | display: block; 14 | font-size: medium; 15 | line-height: var(--line-height); 16 | text-align: var(--justify-normal); 17 | border-right: none; 18 | border-left: 0.25rem solid var(--color-accent); 19 | padding: 0 0 0 1rem; 20 | margin-left: 0; 21 | max-width: 100%; 22 | } 23 | 24 | blockquote p { 25 | border-left: none; 26 | } 27 | 28 | blockquote footer { 29 | color: var(--color-text); 30 | display: block; 31 | font-size: small; 32 | line-height: var(--line-height); 33 | padding: 1.5rem 0; 34 | } 35 | 36 | a { 37 | text-decoration: none; 38 | word-break: break-word; 39 | } 40 | 41 | * { 42 | scrollbar-width: auto; 43 | } 44 | 45 | nav { 46 | margin-bottom: 1rem; 47 | } 48 | 49 | p img, 50 | pre, 51 | pre code, 52 | pre samp { 53 | max-width: 100%; 54 | } 55 | 56 | table td[align], 57 | table th[align] { 58 | text-align: left !important; 59 | } 60 | 61 | table td[align="center"], 62 | table th[align="center"] { 63 | text-align: center !important; 64 | } 65 | 66 | table td[align="right"], 67 | table th[align="right"] { 68 | text-align: right !important; 69 | } -------------------------------------------------------------------------------- /markdown-blog/2024-12-26.md: -------------------------------------------------------------------------------- 1 | # Setup guide 2 | 3 | It's not an exaggeration - you really can build your own website in less than 60 seconds using SkunkHTML. You just need to know where to click. 4 | 5 | ### 1. Login to GitHub 6 | 7 | Log in to GitHub and fork SkunkHTML [repository](https://github.com/mg0x7BE/skunk-html): 8 | 9 | ![1-create-a-new-fork.png](images/1-create-a-new-fork.png) 10 | 11 | ### 2. Name your repo 12 | 13 | Name your new repository. If you haven't used GitHub Pages before, name it `.github.io` (where `` is your GitHub username). This way your new site will be available at `http://.github.io`. 14 | 15 | ![2-name-your-repo.png](images/2-name-your-repo.png) 16 | 17 | If you already have this address taken because you're using another site, you can use any name, for example `just-a-test`. Then the site will be available at `http://.github.io/just-a-test`. 18 | 19 | ### 3. Enable workflows 20 | 21 | Make sure that workflows are enabled in the Actions tab. If not, be sure to enable them: 22 | 23 | ![3-enable-workflows.png](images/3-enable-workflows.png) 24 | 25 | ### 4. Enable GitHub Pages 26 | 27 | GitHub Pages is the service responsible for hosting your website on GitHub (for free). We need to enable it too: 28 | 29 | ![4-enable-pages.png](images/4-enable-pages.png) 30 | 31 | ### 5. Run your new workflow. 32 | 33 | If your workflow hasn't had any runs before, run it: 34 | 35 | ![5-run-workflow.png](images/5-run-workflow.png) 36 | 37 | If you already had a website before, it's possible that the workflow ran but didn't work because GitHub Pages wasn't enabled in this repo yet. In that case, simply run it again now: 38 | 39 | ![5-re-run-if-needed.png](images/5-re-run-if-needed.png) 40 | 41 | ### 6. Done! 42 | 43 | Done! Your new site should appear shortly at the address that corresponds to your repository name (see step 2). -------------------------------------------------------------------------------- /Program.fs: -------------------------------------------------------------------------------- 1 | open System.IO 2 | open SkunkUtils 3 | open SkunkHtml 4 | 5 | [] 6 | let main argv = 7 | argv |> ignore 8 | 9 | if not (Directory.Exists(Config.markdownDir)) then 10 | printfn $"Markdown directory does not exist : {Config.markdownDir}" 11 | failwith "Markdown directory not found" 12 | 13 | if not (Directory.Exists(Config.outputDir)) then 14 | printfn $"Creating {Path.GetFileName Config.outputDir} folder" 15 | Directory.CreateDirectory(Config.outputDir) 16 | |> ignore 17 | 18 | let header = Disk.readFile (Path.Combine(Config.htmlDir, "header.html")) 19 | let footer = Disk.readFile (Path.Combine(Config.htmlDir, "footer.html")) 20 | 21 | let allMarkdownFiles = Directory.GetFiles(Config.markdownDir, "*.md") 22 | 23 | let blogArticleFiles = 24 | allMarkdownFiles 25 | |> Array.filter isArticle 26 | 27 | let listOfAllBlogArticles = 28 | blogArticleFiles 29 | |> Array.map (fun file -> 30 | let date = Path.GetFileNameWithoutExtension(file) 31 | let title = extractTitleFromMarkdownFile(file) 32 | let urlFriendlyTitle = Url.toUrlFriendly title 33 | (date, title, $"{urlFriendlyTitle}.html")) 34 | |> Array.sortByDescending (fun (date, _, _) -> date) 35 | |> Array.toList 36 | 37 | let createBlogArticlePages () = 38 | blogArticleFiles 39 | |> Array.iter (createPage header footer) 40 | 41 | let createOtherPages () = 42 | allMarkdownFiles 43 | |> Array.filter (fun file -> not (isArticle file)) 44 | |> Array.filter (fun file -> Path.GetFileName(file) <> Config.frontPageMarkdownFileName) 45 | |> Array.iter (createPage header footer) 46 | 47 | createIndexPage header footer listOfAllBlogArticles 48 | createOtherPages () 49 | createBlogArticlePages () 50 | 51 | 52 | Disk.copyFolderToOutput Config.fontsDir Config.outputFontsDir 53 | Disk.copyFolderToOutput Config.cssDir Config.outputCssDir 54 | Disk.copyFolderToOutput Config.imagesDir Config.outputImagesDir 55 | Disk.copyFolderToOutput Config.assetsDir Config.outputAssetsDir 56 | Disk.copyFolderToOutput Config.scriptsDir Config.outputScriptsDir 57 | 58 | printf "\nBuild complete. Your site is ready for deployment!" 59 | 0 60 | -------------------------------------------------------------------------------- /SkunkUtils.fs: -------------------------------------------------------------------------------- 1 | module SkunkUtils 2 | 3 | module Config = 4 | open System.IO 5 | 6 | let sourceDir = __SOURCE_DIRECTORY__ 7 | 8 | let markdownDir = Path.Combine(sourceDir, "markdown-blog") 9 | let htmlDir = Path.Combine(sourceDir, "html") 10 | let outputDir = Path.Combine(sourceDir, "skunk-html-output") 11 | 12 | let cssDir = Path.Combine(sourceDir, "css") 13 | let outputCssDir = Path.Combine(outputDir, "css") 14 | 15 | let fontsDir = Path.Combine(sourceDir, "fonts") 16 | let outputFontsDir = Path.Combine(outputDir, "fonts") 17 | 18 | let imagesDir = Path.Combine(markdownDir, "images") 19 | let outputImagesDir = Path.Combine(outputDir, "images") 20 | 21 | let assetsDir = Path.Combine(sourceDir, "assets") 22 | let outputAssetsDir = Path.Combine(outputDir, "assets") 23 | 24 | let scriptsDir = Path.Combine(sourceDir, "scripts") 25 | let outputScriptsDir = Path.Combine(outputDir, "scripts") 26 | 27 | let frontPageMarkdownFileName = "index.md" 28 | 29 | module Disk = 30 | open System.IO 31 | 32 | let readFile (path: string) = 33 | path 34 | |> File.Exists 35 | |> function 36 | | true -> File.ReadAllText(path) 37 | | false -> "" 38 | 39 | let writeFile (path: string) (content: string) = 40 | File.WriteAllText(path, content) 41 | printfn $"Generated: {Path.GetFileName path} -> {path}\n" 42 | 43 | let copyFolderToOutput (sourceFolder: string) (destinationFolder: string) = 44 | if not (Directory.Exists(sourceFolder)) then 45 | printfn $"Source folder does not exist: {sourceFolder}" 46 | else 47 | if not (Directory.Exists(destinationFolder)) then 48 | Directory.CreateDirectory(destinationFolder) 49 | |> ignore 50 | 51 | Directory.GetFiles(sourceFolder) 52 | |> Array.iter (fun file -> 53 | let fileName = Path.GetFileName(file) 54 | let destFile = Path.Combine(destinationFolder, fileName) 55 | printfn $"Copying: {fileName} -> {destFile}" 56 | File.Copy(file, destFile, true)) 57 | 58 | module Url = 59 | open System.Text.RegularExpressions 60 | 61 | let toUrlFriendly (input: string) = 62 | input.ToLowerInvariant() 63 | |> fun text -> Regex.Replace(text, @"[^\w\s]", "") // Remove all non-alphanumeric characters 64 | |> fun text -> Regex.Replace(text, @"\s+", "-") // Replace spaces with hyphens -------------------------------------------------------------------------------- /markdown-blog/2024-12-27.md: -------------------------------------------------------------------------------- 1 | # Customization 2 | 3 | OK, you've read the [Setup guide](setup-guide.html) and [Blogging with Markdown](blogging-with-markdown.html). You have your site online, everything works beautifully but you're wondering how to adapt it to your needs. It's very simple, we just need to change a few things. 4 | 5 | ## Title 6 | 7 | The site title, which appears as the browser tab name, can be found in the `/html/` folder in the `title.html` file. 8 | 9 | ## Avatar and icons 10 | 11 | The avatar at the top can be found in the `/assets/` folder as `avatar.jpg`. You can replace it with another one. Next to it are icons for various browsers and devices, such as favicon.svg. You can replace these files with others. 12 | 13 | You can also use a different set of images and formats. In that case, make sure to update the header in the `/html/` folder in the `head.html` file. If you google for "favicon generator" you'll find millions of sites that generate these types of assets for different devices. 14 | 15 | ## More sections! 16 | 17 | You can find how to create blog posts in the [Blogging with Markdown](blogging-with-markdown.html) article. However, if you'd like to add a new tab (next to "Home" and "Links"), you just need to create a new markdown file and add a reference to it in the `header.html` file in the `/html/` folder. 18 | 19 | For example, create a file called `whatever.md` with this content: 20 | 21 | ``` 22 | # Favorite food 23 | This page will contain a list of my favorite dishes 24 | ``` 25 | 26 | When you save this file in the `/markdown-blog/` folder, the build process will generate a `favorite-food.html` (file name based on the title inside the file). 27 | 28 | So the only thing left to do is add a link in the `header.html` file in the `/html/` folder: 29 | 30 | ```html 31 | 36 | ``` 37 | ## Colors and CSS 38 | 39 | If you'd like to play with colors and CSS changes, you'll find everything in the `/css/` folder. There are two files: 40 | 41 | - `styles.css` is the original, unmodified MVP.css v1.17 - https://github.com/andybrewer/mvp 42 | - `tweaks.css` contains small tweaks that override the above settings in several places. You can modify this freely or even delete it and simply insert your own styles. 43 | 44 | You can find references to these files in the `/html/` folder in `head.html`. You can completely replace these CSS files with something else. 45 | 46 | You can see how such a modified site might look here: https://mg0x7BE.github.io/ 47 | 48 | 49 | ## Blog comments system 50 | 51 | SkunkHTML integrates beautifully with [giscus](https://giscus.app/). 52 | It allows to add comments under each blog post. Everything about this topic can be found on the giscus website. All you need to know is that you'll find the configuration script in the `/html/` folder in the `script_giscus.html` file. It's currently commented out. Remove the arrows and adjust it according to what's described on the giscus website. 53 | 54 | As above, you can see an example of such integration at https://mg0x7BE.github.io/ 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![GitHub repo size](https://img.shields.io/github/repo-size/mg0x7BE/skunk-html) 3 | ![GitHub License](https://img.shields.io/github/license/mg0x7BE/skunk-html) 4 | ![GitHub Created At](https://img.shields.io/github/created-at/mg0x7BE/skunk-html) 5 | ![GitHub forks](https://img.shields.io/github/forks/mg0x7BE/skunk-html) 6 | ![GitHub Repo stars](https://img.shields.io/github/stars/mg0x7BE/skunk-html) 7 | 8 | # SkunkHTML 9 | 10 | Automatically generate a website on GitHub Pages using Markdown files. 11 | 12 | ![SkunkHTML](https://mg0x7BE.github.io/skunk-html/images/skunk-final.png) 13 | 14 | Markdown in, GitHub Pages out! 15 | 16 | ## SkunkHTML setup 17 | Your own Markdown blog on GitHub. 18 | 19 | 1. Fork [SkunkHTML](https://github.com/mg0x7BE/skunk-html) repository on GitHub. 20 | 2. Enable GitHub Pages in the repository settings (choose GitHub Actions as the source). 21 | 3. Done. Your blog is online. Example: https://mg0x7BE.github.io/skunk-html/ 22 | 23 | Upload Markdown (.md) files to publish new posts. 24 | 25 | ## How it works 26 | 27 | When a Markdown (.md) file is created and placed in the `/markdown-blog/` folder, the rest happens automatically. GitHub Actions detects changes pushed to the repository, triggers the build process, and deploys the updated site. 28 | 29 | ## Some technical details 30 | 31 | - Blog articles and other content are written in Markdown, allowing for easy content creation and management. These Markdown files are automatically converted to HTML during the build process using F# and the [FSharp.Formatting](https://github.com/fsprojects/FSharp.Formatting) library. 32 | 33 | - The deployment process is fully automated using [GitHub Actions](https://github.com/features/actions). Any changes to this repository are immediately reflected on the live site. 34 | 35 | - [Giscus](https://giscus.app/) comment system is supported. 36 | 37 | - The repository is 100% ready to work directly on GitHub without the need to download it locally. Simply fork it to create your own website. Don't forget to enable GitHub Pages in repo! (Settings ➔ Pages ➔ Build and deployment: "GitHub Actions"). 38 | 39 | ## Folder structure 40 | 41 | - `/`: Root directory of the project. 42 | 43 | - `.github/workflows/`: GitHub Actions workflow file. Responsible for automatically generating final website on GitHub Pages 44 | - `assets/`: Files used across the entire site, such as the avatar, favicon, and other shared resources. 45 | - `css/`: CSS files for the site. 46 | - `fonts/`: Custom fonts go here. 47 | - `html/`: HTML fragments used throughout the site, such as the title and footer. 48 | - `markdown-blog/`: Directory containing the Markdown files for articles and other content. Blog articles are identified by file names that start with a digit. 49 | - `images/`: Images used in the articles. 50 | - `scripts/`: Syntax highlighting script and optionally other custom scripts.. 51 | - `skunk-html-output/`: Directory that will be created during the build process, with the generated HTML files. 52 | 53 | - `LICENSE`: License file for the project. 54 | - `Program.fs`: F# program that handles the generation of HTML from Markdown 55 | - `README.md`: This file. 56 | - `skunk-html.fsproj`: Project file 57 | 58 | ## Examples 59 | 60 | Detailed examples can be found at: https://mg0x7BE.github.io/skunk-html 61 | 62 | ## Contributing 63 | 64 | Suggestions, bug reports, and pull requests welcome. Use discussions, issues, or PRs. 65 | 66 | ## License 67 | 68 | This project is licensed under the terms of the [Unlicense](https://en.wikipedia.org/wiki/Unlicense). 69 | 70 | ## Dependencies 71 | 72 | - [MVP.css](https://github.com/andybrewer/mvp) for styling 73 | - [microlight.js](https://github.com/asvd/microlight) for syntax highlighting 74 | - [FSharp.Formatting](https://github.com/fsprojects/FSharp.Formatting) library for Markdown processing 75 | 76 | ## Optional self-hosting and custom build 77 | 78 | To build locally: 79 | 80 | 1. [Download](https://dotnet.microsoft.com/en-us/download) and install .NET on Linux / macOS / Windows 81 | 2. Run the following commands 82 | ``` 83 | git clone https://github.com/mg0x7BE/skunk-html.git 84 | cd skunk-html 85 | dotnet restore 86 | dotnet run 87 | ``` 88 | 3. Done. Your site is in the `skunk-html-output` folder. 89 | -------------------------------------------------------------------------------- /SkunkHtml.fs: -------------------------------------------------------------------------------- 1 | module SkunkHtml 2 | open SkunkUtils 3 | open System.IO 4 | open FSharp.Formatting.Markdown 5 | 6 | let generateFinalHtml (head: string) (header: string) (footer: string) (content: string) (script: string) = 7 | $""" 8 | 9 | 10 | 11 | {head} 12 | 13 | 14 |
15 | {header} 16 |
17 |
18 | {content} 19 |
20 |
21 |
22 | {footer} 23 |
24 | 27 | 28 | 29 | """ 30 | 31 | let head (titleSuffix: string) = 32 | let headTemplate = 33 | Path.Combine(Config.htmlDir, "head.html") 34 | |> Disk.readFile 35 | 36 | let titleTemplate = 37 | Path.Combine(Config.htmlDir, "title.html") 38 | |> Disk.readFile 39 | 40 | headTemplate.Replace("{{title.html content}}", titleTemplate + titleSuffix) 41 | 42 | let isArticle (file: string) = 43 | System.Char.IsDigit(Path.GetFileName(file).[0]) 44 | 45 | let highlightingScript = 46 | Path.Combine(Config.htmlDir, "script_syntax_highlighting.html") 47 | |> Disk.readFile 48 | 49 | let extractTitleFromMarkdownFile (markdownFilePath: string) = 50 | File.ReadAllLines(markdownFilePath) 51 | |> Array.tryFind _.StartsWith("# ") 52 | |> Option.defaultValue "# No Title" 53 | |> _.TrimStart('#').Trim() 54 | 55 | let createPage (header: string) (footer: string) (markdownFilePath: string) = 56 | let title = extractTitleFromMarkdownFile(markdownFilePath) 57 | let fileName = Url.toUrlFriendly title 58 | let outputHtmlFilePath = Path.Combine(Config.outputDir, fileName + ".html") 59 | let markdownContent = File.ReadAllText(markdownFilePath) 60 | 61 | let htmlContent = 62 | match isArticle markdownFilePath with 63 | | false -> Markdown.ToHtml(markdownContent) 64 | | true -> 65 | let date = Path.GetFileNameWithoutExtension(markdownFilePath) 66 | 67 | let publicationDate = 68 | $"""

Published on

""" 69 | 70 | let giscusScript = 71 | Path.Combine(Config.htmlDir, "script_giscus.html") 72 | |> Disk.readFile 73 | 74 | let mainHtmlContent = Markdown.ToHtml( 75 | markdownContent 76 | + "\n\n" 77 | + publicationDate 78 | + "\n\n" 79 | ) 80 | mainHtmlContent + giscusScript 81 | 82 | let finalHtmlContent = 83 | generateFinalHtml (head (" - " + title)) header footer htmlContent highlightingScript 84 | 85 | printfn $"Processing {Path.GetFileName markdownFilePath} ->" 86 | Disk.writeFile outputHtmlFilePath finalHtmlContent 87 | 88 | let createIndexPage (header: string) (footer: string) (listOfAllBlogArticles: (string * string * string) list) = 89 | let frontPageMarkdownFilePath = Path.Combine(Config.markdownDir, Config.frontPageMarkdownFileName) 90 | 91 | let frontPageContentHtml = 92 | if File.Exists(frontPageMarkdownFilePath) then 93 | printfn $"Processing {Path.GetFileName frontPageMarkdownFilePath} ->" 94 | Markdown.ToHtml(File.ReadAllText(frontPageMarkdownFilePath)) 95 | else 96 | printfn $"Warning! File {Config.frontPageMarkdownFileName} does not exist! The main page will only contain blog entries, without a welcome message" 97 | "" 98 | 99 | let listOfAllBlogArticlesContentHtml = 100 | listOfAllBlogArticles 101 | |> List.map (fun (date, title, link) -> $"""
  • {date}: {title}
  • """) 102 | |> String.concat "\n" 103 | 104 | let content = 105 | $""" 106 | {frontPageContentHtml} 107 |
    108 |

    blog entries

    109 |
      110 | {listOfAllBlogArticlesContentHtml} 111 |
    112 |
    113 | """ 114 | 115 | let frontPageHtmlContent = generateFinalHtml (head "") header footer content highlightingScript 116 | let indexHtmlFilePath = Path.Combine(Config.outputDir, "index.html") 117 | 118 | Disk.writeFile indexHtmlFilePath frontPageHtmlContent -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]utput/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | # but not Directory.Build.rsp, as it configures directory-level build defaults 87 | !Directory.Build.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml 402 | .idea/ 403 | 404 | # MacOS system file 405 | .DS_Store 406 | 407 | # Skunk HTML output 408 | [Ss]kunk-html-output/ -------------------------------------------------------------------------------- /markdown-blog/2024-12-25.md: -------------------------------------------------------------------------------- 1 | # Blogging with Markdown 2 | 3 | Markdown is an ultra-simple markup language that allows you to format text using any editor. When you place such a file (.md) in the `/markdown-blog/` folder, it automatically becomes a blog post. 4 | 5 | Below you'll find some important information specific to SkunkHTML, as well as a comprehensive tutorial on how to write Markdown. 6 | 7 | ## Creating a blog post 8 | 9 | Create a new text file and make a blog post header inside. In Markdown, headers are marked with the `#` symbol. For example: 10 | 11 | ``` 12 | # Testing SkunkHTML 13 | 14 | I don't know how it works yet, but I'll find out! 15 | ``` 16 | Name this file using a date in any format, but it's important that it starts with a number. For example `2024-12-27.md` (.md is the standard extension for Markdown files) 17 | 18 | Place this file in the `/markdown-blog/` folder. If you're doing this on GitHub with Pages enabled, you'll see your new post appear on your site shortly: 19 | 20 | ![first-blog-post.png](images/first-blog-post.png) 21 | 22 | It's that simple! Just remember that you use `#` to mark your page title, and to use date in file name. Later in the file, you can use additional headers freely. Only the first one matters for the blog post title. 23 | 24 | Markdown is super convenient if you just want to write something without fiddling with HTML, and that's exactly how SkunkHTML works. 25 | 26 | --- 27 | 28 | # Headers 29 | 30 | ``` 31 | # This is heading 1 32 | ## This is heading 2 33 | ### This is heading 3 34 | #### This is heading 4 35 | ##### This is heading 5 36 | ###### This is heading 6 37 | ``` 38 | 39 | # This is heading 1 40 | ## This is heading 2 41 | ### This is heading 3 42 | #### This is heading 4 43 | ##### This is heading 5 44 | ###### This is heading 6 45 | 46 | --- 47 | 48 | # Lists 49 | 50 | ``` 51 | * Unordered lists will start with asterisks 52 | - Or with dashes 53 | + Or with plus signs 54 | 55 | 1. To create an ordered list, You can start lines with numbers followed by periods 56 | 1. Indent with 4 spaces or a tab to create sub-items 57 | 2. Use further indentation to create nested lists 58 | - It's possible to mix and match list types 59 | 60 | 2. Sub-lists within unordered lists will use different markers automatically: 61 | * Any marker can be used for the first level 62 | + And any marker for new sub-levels: 63 | * You can use asterisks 64 | + Or plus signs 65 | - Or dashes 66 | ``` 67 | 68 | * Unordered lists will start with asterisks 69 | - Or with dashes 70 | + Or with plus signs 71 | 72 | 1. To create an ordered list, You can start lines with numbers followed by periods 73 | 1. Indent with 4 spaces or a tab to create sub-items 74 | 2. Use further indentation to create nested lists 75 | - It's possible to mix and match list types 76 | 77 | 2. Sub-lists within unordered lists will use different markers automatically: 78 | * Any marker can be used for the first level 79 | + And any marker for new sub-levels: 80 | * You can use asterisks 81 | + Or plus signs 82 | - Or dashes 83 | 84 | --- 85 | 86 | # Emphasis 87 | 88 | ``` 89 | Italics: *single asterisks* or _single underscores_ 90 | 91 | Bold: **double asterisks** or __double underscores__ 92 | 93 | Combined bold and italics: **_double asterisks and single underscores_** 94 | 95 | All bold and italic: ***triple asterisks*** or ___triple underscores___ 96 | ``` 97 | 98 | Italics: *single asterisks* or _single underscores_ 99 | 100 | Bold: **double asterisks** or __double underscores__ 101 | 102 | Combined bold and italics: **_double asterisks and single underscores_** 103 | 104 | All bold and italic: ***triple asterisks*** or ___triple underscores___ 105 | 106 | --- 107 | 108 | # Links 109 | 110 | ``` 111 | [Example website](https://www.example.net) 112 | 113 | [Same website with a title](https://www.example.net "Example Website") 114 | 115 | [Link to a file in the project](./images/skunk-final.png) 116 | 117 | [Numbers can be used for reference links][1] 118 | 119 | Or simply [text as the reference]. 120 | 121 | URLs will automatically become links: https://www.example.net or . 122 | 123 | ... and here are some reference links defined later in the document that won't be visible in the rendered HTML: 124 | 125 | [1]: https://www.example.net 126 | [text as the reference]: https://www.example.net 127 | ``` 128 | 129 | [Example website](https://www.example.net) 130 | 131 | [Same website with a title](https://www.example.net "Example Website") 132 | 133 | [Link to a file in the project](./images/skunk-final.png) 134 | 135 | [Numbers can be used for reference links][1] 136 | 137 | Or simply [text as the reference]. 138 | 139 | URLs will automatically become links: https://www.example.net or . 140 | 141 | ... and here are some reference links defined later in the document that won't be visible in the rendered HTML: 142 | 143 | [1]: https://www.example.net 144 | [text as the reference]: https://www.example.net 145 | 146 | --- 147 | 148 | # Images 149 | 150 | Please place all Markdown-related image files in the `markdown-blog/images` folder: 151 | 152 | ``` 153 | Inline, with title: 154 | ![eye image](images/eye.png "The Eye") 155 | 156 | Inline, reference, with title: 157 | ![eye image][eye_image_reference] 158 | 159 | [eye_image_reference]: images/eye.png "The Eye" 160 | 161 | Regular: 162 | 163 | ![Skunk drawing](images/skunk-drawing.jpg) 164 | 165 | Regular, with title: 166 | 167 | ![Skunk final](images/skunk-final.png "Skunk") 168 | ``` 169 | 170 | Inline, with title: 171 | ![eye image](images/eye.png "The Eye") 172 | 173 | Inline, reference, with title: 174 | ![eye image][eye_image_reference] 175 | 176 | [eye_image_reference]: images/eye.png "The Eye" 177 | 178 | Regular: 179 | 180 | ![Skunk drawing](images/skunk-drawing.jpg) 181 | 182 | Regular, with title: 183 | 184 | ![Skunk final](images/skunk-final.png "Skunk") 185 | 186 | --- 187 | 188 | # Ignoring formatting 189 | 190 | It is possible to ignore Markdown fomatting by placing a backslash before the Markdown character. 191 | 192 | ``` 193 | For example, \*this text won't be italicized\*. 194 | ``` 195 | For example, \*this text won't be italicized\*. 196 | 197 | --- 198 | 199 | # Code blocks 200 | 201 | ``` 202 | Backticks can be used to highlight code inline, like this: 203 | 204 | "To print "Hello, F#!" in F#, you can use the `printfn` function". 205 | 206 | And triple backticks can be used to highlight code blocks: 207 | 208 | ```fsharp 209 | let greet name = 210 | printfn "Hello, %s!" name 211 | 212 | greet "F#" 213 | 214 | ``` 215 | ``` 216 | 217 | Backticks can be used to highlight code inline, like this: 218 | 219 | "To print "Hello, F#!" in F#, you can use the `printfn` function". 220 | 221 | And triple backticks can be used to highlight code blocks: 222 | 223 | ```fsharp 224 | let greet name = 225 | printfn "Hello, %s!" name 226 | 227 | greet "F#" 228 | ``` 229 | Remember to specify the language name immediately after the three backticks (in this case fsharp) if you want syntax highlighting to work! 230 | 231 | --- 232 | 233 | # Tables 234 | 235 | ``` 236 | Tables are super easy to create: 237 | 238 | | Left-Aligned | Center-Aligned | Right-Aligned | 239 | |:-------------|:--------------:|--------------:| 240 | | This | This | This | 241 | | column | column | column | 242 | | is | is | is | 243 | | left-aligned | center-aligned | right-aligned | 244 | ``` 245 | 246 | Tables are super easy to create: 247 | 248 | | Left-Aligned | Center-Aligned | Right-Aligned | 249 | |:-------------|:--------------:|--------------:| 250 | | This | This | This | 251 | | column | column | column | 252 | | is | is | is | 253 | | left-aligned | center-aligned | right-aligned | 254 | --- 255 | 256 | # Blockquotes 257 | 258 | ``` 259 | > This is a blockquote. 260 | > 261 | > This is another paragraph inside the same blockquote. 262 | > 263 | > > This is a nested blockquote. 264 | > > 265 | > > Another paragraph inside the nested blockquote. 266 | > > 267 | > > > This is a nested blockquote inside the second level. 268 | > > > 269 | > > > Another paragraph inside the third level blockquote. 270 | > > 271 | > > Back to the second level blockquote. 272 | > 273 | > Back to the original blockquote. 274 | ``` 275 | 276 | > This is a blockquote. 277 | > 278 | > This is another paragraph inside the same blockquote. 279 | > 280 | > > This is a nested blockquote. 281 | > > 282 | > > Another paragraph inside the nested blockquote. 283 | > > 284 | > > > This is a nested blockquote inside the second level. 285 | > > > 286 | > > > Another paragraph inside the third level blockquote. 287 | > > 288 | > > Back to the second level blockquote. 289 | > 290 | > Back to the original blockquote. 291 | 292 | 293 | --- 294 | 295 | # Inline HTML 296 | 297 | ``` 298 | Inline HTML is partially supported. Sometimes it can be useful, for example to strikethrough text: 299 | 300 | This text will be striked through. 301 | 302 | For some reason regular strikethrough is not supported. Proof: ~~This text is not striked through~~. 303 | ``` 304 | 305 | Inline HTML is partially supported. Sometimes it can be useful, for example to strikethrough text: 306 | 307 | This text will be striked through. 308 | 309 | For some reason regular strikethrough is not supported. Proof: ~~This text is not striked through~~. 310 | 311 | --- 312 | 313 | # Horizontal rules 314 | 315 | ``` 316 | Horizontal rule can be created using at least 3 dashes, asterisks, or underscores: 317 | 318 | --- 319 | ``` 320 | 321 | Horizontal rule can be created using at least 3 dashes, asterisks, or underscores: 322 | 323 | --- 324 | That's all for now - you're ready to become a Markdown ninja with 🦨! 325 | 326 | -------------------------------------------------------------------------------- /scripts/microlight.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview microlight - syntax highlightning library 3 | * @version 0.0.7 4 | * 5 | * @license MIT, see http://github.com/asvd/microlight 6 | * @copyright 2016 asvd 7 | * 8 | * Code structure aims at minimizing the compressed library size 9 | */ 10 | 11 | 12 | (function (root, factory) { 13 | if (typeof define === 'function' && define.amd) { 14 | define(['exports'], factory); 15 | } else if (typeof exports !== 'undefined') { 16 | factory(exports); 17 | } else { 18 | factory((root.microlight = {})); 19 | } 20 | }(this, function (exports) { 21 | // for better compression 22 | var _window = window, 23 | _document = document, 24 | appendChild = 'appendChild', 25 | test = 'test', 26 | // style and color templates 27 | textShadow = ';text-shadow:', 28 | opacity = 'opacity:.', 29 | _0px_0px = ' 0px 0px ', 30 | _3px_0px_5 = '3px 0px 5', 31 | brace = ')', 32 | 33 | i, 34 | microlighted, 35 | el; // current microlighted element to run through 36 | 37 | 38 | 39 | var reset = function(nodes) { 40 | // nodes to highlight 41 | microlighted = nodes || _document.getElementsByClassName('microlight'); 42 | 43 | for (i = 0; el = microlighted[i++];) { 44 | var text = el.textContent, 45 | pos = 0, // current position 46 | next1 = text[0], // next character 47 | chr = 1, // current character 48 | prev1, // previous character 49 | prev2, // the one before the previous 50 | token = // current token content 51 | el.innerHTML = '', // (and cleaning the node) 52 | 53 | // current token type: 54 | // 0: anything else (whitespaces / newlines) 55 | // 1: operator or brace 56 | // 2: closing braces (after which '/' is division not regex) 57 | // 3: (key)word 58 | // 4: regex 59 | // 5: string starting with " 60 | // 6: string starting with ' 61 | // 7: xml comment 62 | // 8: multiline comment /* */ 63 | // 9: single-line comment starting with two slashes // 64 | // 10: single-line comment starting with hash # 65 | tokenType = 0, 66 | 67 | // kept to determine between regex and division 68 | lastTokenType, 69 | // flag determining if token is multi-character 70 | multichar, 71 | node, 72 | 73 | // calculating the colors for the style templates 74 | colorArr = /(\d*\, \d*\, \d*)(, ([.\d]*))?/g.exec( 75 | _window.getComputedStyle(el).color 76 | ), 77 | pxColor = 'px rgba('+colorArr[1]+',', 78 | alpha = colorArr[3]||1; 79 | 80 | // running through characters and highlighting 81 | while (prev2 = prev1, 82 | // escaping if needed (with except for comments) 83 | // pervious character will not be therefore 84 | // recognized as a token finalize condition 85 | prev1 = tokenType < 7 && prev1 == '\\' ? 1 : chr 86 | ) { 87 | chr = next1; 88 | next1=text[++pos]; 89 | multichar = token.length > 1; 90 | 91 | // checking if current token should be finalized 92 | if (!chr || // end of content 93 | // types 9-10 (single-line comments) end with a 94 | // newline 95 | (tokenType > 8 && chr == '\n') || 96 | [ // finalize conditions for other token types 97 | // 0: whitespaces 98 | /\S/[test](chr), // merged together 99 | // 1: operators 100 | 1, // consist of a single character 101 | // 2: braces 102 | 1, // consist of a single character 103 | // 3: (key)word 104 | !/[$\w]/[test](chr), 105 | // 4: regex 106 | (prev1 == '/' || prev1 == '\n') && multichar, 107 | // 5: string with " 108 | prev1 == '"' && multichar, 109 | // 6: string with ' 110 | prev1 == "'" && multichar, 111 | // 7: xml comment 112 | text[pos-4]+prev2+prev1 == '-->', 113 | // 8: multiline comment 114 | prev2+prev1 == '*/' 115 | ][tokenType] 116 | ) { 117 | // appending the token to the result 118 | if (token) { 119 | // remapping token type into style 120 | // (some types are highlighted similarly) 121 | el[appendChild]( 122 | node = _document.createElement('span') 123 | ).setAttribute('style', [ 124 | // 0: not formatted 125 | '', 126 | // 1: keywords 127 | textShadow + _0px_0px+9+pxColor + alpha * .7 + '),' + 128 | _0px_0px+2+pxColor + alpha * .4 + brace, 129 | // 2: punctuation 130 | opacity + 6 + 131 | textShadow + _0px_0px+7+pxColor + alpha / 4 + '),' + 132 | _0px_0px+3+pxColor + alpha / 4 + brace, 133 | // 3: strings and regexps 134 | opacity + 7 + 135 | textShadow + _3px_0px_5+pxColor + alpha / 5 + '),-' + 136 | _3px_0px_5+pxColor + alpha / 5 + brace, 137 | // 4: comments 138 | 'font-style:italic;'+ 139 | opacity + 5 + 140 | textShadow + _3px_0px_5+pxColor + alpha / 4 + '),-' + 141 | _3px_0px_5+pxColor + alpha / 4 + brace 142 | ][ 143 | // not formatted 144 | !tokenType ? 0 : 145 | // punctuation 146 | tokenType < 3 ? 2 : 147 | // comments 148 | tokenType > 6 ? 4 : 149 | // regex and strings 150 | tokenType > 3 ? 3 : 151 | // otherwise tokenType == 3, (key)word 152 | // (1 if regexp matches, 0 otherwise) 153 | + /^(a(bstract|lias|nd|rguments|rray|s(m|sert)?|uto)|b(ase|egin|ool(ean)?|reak|yte)|c(ase|atch|har|hecked|lass|lone|ompl|onst|ontinue)|de(bugger|cimal|clare|f(ault|er)?|init|l(egate|ete)?)|do|double|e(cho|ls?if|lse(if)?|nd|nsure|num|vent|x(cept|ec|p(licit|ort)|te(nds|nsion|rn)))|f(allthrough|alse|inal(ly)?|ixed|loat|or(each)?|riend|rom|unc(tion)?)|global|goto|guard|i(f|mp(lements|licit|ort)|n(it|clude(_once)?|line|out|stanceof|t(erface|ernal)?)?|s)|l(ambda|et|ock|ong)|m(icrolight|odule|utable)|NaN|n(amespace|ative|ext|ew|il|ot|ull)|o(bject|perator|r|ut|verride)|p(ackage|arams|rivate|rotected|rotocol|ublic)|r(aise|e(adonly|do|f|gister|peat|quire(_once)?|scue|strict|try|turn))|s(byte|ealed|elf|hort|igned|izeof|tatic|tring|truct|ubscript|uper|ynchronized|witch)|t(emplate|hen|his|hrows?|ransient|rue|ry|ype(alias|def|id|name|of))|u(n(checked|def(ined)?|ion|less|signed|til)|se|sing)|v(ar|irtual|oid|olatile)|w(char_t|hen|here|hile|ith)|xor|yield)$/[test](token) 154 | ]); 155 | 156 | node[appendChild](_document.createTextNode(token)); 157 | } 158 | 159 | // saving the previous token type 160 | // (skipping whitespaces and comments) 161 | lastTokenType = 162 | (tokenType && tokenType < 7) ? 163 | tokenType : lastTokenType; 164 | 165 | // initializing a new token 166 | token = ''; 167 | 168 | // determining the new token type (going up the 169 | // list until matching a token type start 170 | // condition) 171 | tokenType = 11; 172 | while (![ 173 | 1, // 0: whitespace 174 | // 1: operator or braces 175 | /[\/{}[(\-+*=<>:;|\\.,?!&@~]/[test](chr), 176 | /[\])]/[test](chr), // 2: closing brace 177 | /[$\w]/[test](chr), // 3: (key)word 178 | chr == '/' && // 4: regex 179 | // previous token was an 180 | // opening brace or an 181 | // operator (otherwise 182 | // division, not a regex) 183 | (lastTokenType < 2) && 184 | // workaround for xml 185 | // closing tags 186 | prev1 != '<', 187 | chr == '"', // 5: string with " 188 | chr == "'", // 6: string with ' 189 | // 7: xml comment 190 | chr+next1+text[pos+1]+text[pos+2] == '