Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
30 changes: 13 additions & 17 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
name: Deploy Jekyll site to Pages
# Build the VitePress site and deploy it to GitHub Pages
name: Deploy site to Pages

on:
# Runs on pushes targeting the default branch
Expand Down Expand Up @@ -29,29 +29,25 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for the last modified date of the pages
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: npm
- name: Install Node deps
run: npm ci
- name: Compile typescript
run: npm run build
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0' # Not needed with a .ruby-version file
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
cache-version: 0 # Increment this number if you need to re-download cached gems
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
- run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" # defaults output to '/_site'
env:
JEKYLL_ENV: production
- name: Build with VitePress
run: npm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3 # This will automatically upload an artifact from the '/_site' directory
uses: actions/upload-pages-artifact@v3
with:
path: .vitepress/dist
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
31 changes: 16 additions & 15 deletions .github/workflows/preview-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ name: preview-pr
on:
pull_request_target:
types: [opened, reopened, synchronize]

permissions:
pull-requests: write
pull-requests: write

jobs:
authorize:
environment:
Expand All @@ -16,32 +16,33 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: "true"

build:
needs: authorize
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Ruby
uses: ruby/setup-ruby@v1
fetch-depth: 0
persist-credentials: false
- name: Setup node
uses: actions/setup-node@v4
with:
ruby-version: '3.0'
bundler-cache: true
- run: bundle exec jekyll build --baseurl ""
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- name: Publish to Cloudflare Pages
id: preview-pages
uses: cloudflare/pages-action@v1
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT }}
projectName: hack-gpon-preview
directory: _site
branch: preview
command: pages deploy .vitepress/dist --project-name=hack-gpon-preview --branch=preview
- uses: thollander/actions-comment-pull-request@v2
with:
message: |
Preview of the website obtained from the PR: ${{ steps.preview-pages.outputs.url }}
Preview of the website obtained from the PR: ${{ steps.preview-pages.outputs.deployment-url }}

14 changes: 6 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
### Jekyll ###
_site/
.sass-cache/
.jekyll-cache/
.jekyll-metadata
Gemfile.lock
assets/js/zzzz-search-data.json
### VitePress ###
.vitepress/dist/
.vitepress/cache/
.vitepress/.temp/

### Node ###
node_modules/
assets/js/generated
128 changes: 128 additions & 0 deletions .vitepress/config.mts
Comment thread
simonebortolin marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import fs from 'node:fs'
import path from 'node:path'
import { createMarkdownRenderer, defineConfig, type MarkdownRenderer } from 'vitepress'
import { configureMarkdown, extractHardwareSpecs } from './markdown'
import { isExternal, pages, resolveLink, rewrites, sidebar, srcExclude } from './pages'

const hostname = 'https://hack-gpon.org'
const repository = 'https://github.com/hack-gpon/hack-gpon.github.io'

let specsRenderer: Promise<MarkdownRenderer> | undefined

const telegramIcon =
'<svg role="img" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><title>Telegram</title><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8.287 5.906c-.778.324-2.334.994-4.666 2.01-.378.15-.577.298-.595.442-.03.243.275.339.69.47l.175.055c.408.133.958.288 1.243.294.26.006.549-.1.868-.32 2.179-1.471 3.304-2.214 3.374-2.23.05-.012.12-.026.166.016.047.041.042.12.037.141-.03.129-1.227 1.241-1.846 1.817-.193.18-.33.307-.358.336a8.154 8.154 0 0 1-.188.186c-.38.366-.664.64.015 1.088.327.216.589.393.85.571.284.194.568.387.936.629.093.06.183.125.27.187.331.236.63.448.997.414.214-.02.435-.22.547-.82.265-1.417.786-4.486.906-5.751a1.426 1.426 0 0 0-.013-.315.337.337 0 0 0-.114-.217.526.526 0 0 0-.31-.093c-.3.005-.763.166-2.984 1.09z"/></svg>'

export default defineConfig({
lang: 'en-US',
title: 'Hack GPON',
description: 'Worldwide wiki on how to access, change and edit ONTs',

srcExclude,
rewrites,
cleanUrls: true,
lastUpdated: true,
sitemap: {
hostname,
// the redirect pages are not real pages (as with jekyll-redirect-from)
transformItems: (items) => {
const redirects = new Set(pages.filter((p) => p.frontmatter.redirect_to).map((p) => p.url.slice(1)))
return items.filter((item) => !redirects.has(decodeURI(item.url)))
}
},

head: [
['link', { rel: 'icon', href: '/favicon.ico', sizes: '48x48' }],
['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' }],
['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' }],
['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }],
['link', { rel: 'manifest', href: '/site.webmanifest' }],
['link', { rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#27262b' }],
['meta', { name: 'msapplication-TileColor', content: '#27262b' }],
['meta', { name: 'theme-color', content: '#27262b' }]
],

transformHead({ pageData }) {
const head: [string, Record<string, string>][] = []
const page = pages.find((p) => p.file === pageData.filePath)
const redirect = pageData.frontmatter.redirect_to
if (redirect) {
const target = (page && resolveLink(page.url)) ?? redirect
head.push(['meta', { 'http-equiv': 'refresh', content: `0; url=${target}` }])
head.push(['link', { rel: 'canonical', href: isExternal(target) ? target : hostname + target }])
} else if (page) {
head.push(['link', { rel: 'canonical', href: hostname + page.url }])
}
return head
},

markdown: {
config: configureMarkdown,
// in dev the cache of the rendered pages is not cleared for the rewritten pages when a
// partial changes, so the page would not be updated
cache: process.argv[2] !== 'dev'
},

// the "Hardware Specifications" table is also rendered in the aside, above the outline
async transformPageData(pageData, { siteConfig }) {
const file = path.join(siteConfig.srcDir, pageData.filePath)
if (!fs.existsSync(file)) return
const table = extractHardwareSpecs(fs.readFileSync(file, 'utf8'))
if (!table) return
specsRenderer ??= createMarkdownRenderer(siteConfig.srcDir, siteConfig.markdown, siteConfig.site.base, siteConfig.logger)
pageData.hardwareSpecs = (await specsRenderer).render(table, { path: file, relativePath: pageData.relativePath })
},

vite: {
build: {
// the big chunks (search index, mermaid, pages with serial dumps) are loaded on demand
chunkSizeWarningLimit: 1000
}
},

themeConfig: {
logo: { src: '/favicon-32x32.png', alt: '' },

nav: [
{ text: 'Quick Start', link: '/quick-start/' },
{ text: 'FAQ', link: '/faq/' }
],

sidebar: sidebar(),

socialLinks: [
{ icon: 'github', link: repository, ariaLabel: 'GitHub' },
{ icon: { svg: telegramIcon }, link: 'https://t.me/HackGPON', ariaLabel: 'Telegram' }
],

editLink: {
pattern: `${repository}/edit/main/:path`,
text: 'Edit this page on GitHub'
},

lastUpdated: {
text: 'Last Modified',
formatOptions: { dateStyle: 'medium' }
},

search: {
provider: 'local',
options: {
_render(src, env, md) {
const html = md.render(src, env)
if (env.frontmatter?.search === false || env.frontmatter?.redirect_to) return ''
// footnote references in the headings would be taken as section anchors
return html.replace(/<sup class="footnote-ref">.*?<\/sup>/g, '')
}
}
},

outline: {
level: [1, 3]
},

docFooter: {
prev: false,
next: false
}
}
})
115 changes: 115 additions & 0 deletions .vitepress/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import fs from 'node:fs'
import path from 'node:path'
import { Liquid } from 'liquidjs'
import type MarkdownIt from 'markdown-it'
import footnote from 'markdown-it-footnote'
import { resolveLink } from './pages'

const liquid = new Liquid()

/**
* Parametrized partials.
*
* <!--@partial: ./_partials/ont-nokia-use.md
* username: "ONTUSER"
* -->
*
* The partial is a Liquid template, the parameters are available as `include.<name>`.
* Every parameter is on its own line: `name: value`, the value can be a JSON string.
*/
export function expandPartials(src: string, file: string, includes?: string[]): string {
return src.replace(/<!--\s*@partial:\s*(\S+?)\s*(?:\n([\s\S]*?))?-->/g, (_, target: string, rawParams = '') => {
const params: Record<string, unknown> = {}
for (const line of rawParams.split(/\r?\n/)) {
const kv = /^\s*([A-Za-z_][\w-]*):\s*(.*?)\s*$/.exec(line)
if (!kv) continue
try {
params[kv[1]] = JSON.parse(kv[2])
} catch {
params[kv[1]] = kv[2]
}
}
const partialFile = path.resolve(path.dirname(file), target)
// registered as dependency of the page, like the VitePress includes: the dev server reloads it
includes?.push(partialFile.replace(/\\/g, '/'))
const template = fs.readFileSync(partialFile, 'utf8')
return liquid.parseAndRenderSync(template, { include: params })
})
}

/**
* Internal links point to the canonical URL of the page, with the trailing slash (as Jekyll did),
* and links to the redirect pages point directly to their destination.
*/
function canonicalLinks(md: MarkdownIt) {
md.core.ruler.push('canonical_links', (state) => {
for (const block of state.tokens) {
for (const token of block.children ?? []) {
if (token.type !== 'link_open') continue
const href = token.attrGet('href')
if (!href || !href.startsWith('/') || href.startsWith('//')) continue
const url = resolveLink(href)
if (url) token.attrSet('href', url)
}
}
})
}

const hardwareSpecsHeading = /^#{1,6}[ \t]+Hardware Specifications[ \t]*$/m

/** Markdown of the table under the "Hardware Specifications" heading, shown in the aside. */
export function extractHardwareSpecs(src: string): string | undefined {
const heading = hardwareSpecsHeading.exec(src)
if (!heading) return
const lines = src.slice(heading.index + heading[0].length).split(/\r?\n/)
let i = 0
while (i < lines.length && lines[i].trim() === '') i++
const table: string[] = []
while (i < lines.length && lines[i].trim().startsWith('|')) table.push(lines[i++])
return table.length > 2 ? table.join('\n') : undefined
}

/** Marks the "Hardware Specifications" table, hidden in the page when it is shown in the aside. */
function hardwareSpecs(md: MarkdownIt) {
md.core.ruler.push('hardware_specs', (state) => {
const tokens = state.tokens
const heading = tokens.findIndex(
(t, i) => t.type === 'heading_open' && tokens[i + 1]?.content.trim() === 'Hardware Specifications'
)
if (heading === -1) return
// the table must follow the heading directly
const table = tokens[heading + 3]
if (table?.type === 'table_open') table.attrJoin('class', 'hardware-specs')
})
// the VitePress renderer of the tables drops the attributes
const tableOpen = md.renderer.rules.table_open!
md.renderer.rules.table_open = (tokens, idx, options, env, self) => {
const html = tableOpen(tokens, idx, options, env, self)
const cls = tokens[idx].attrGet('class')
return cls ? html.replace('<table', `<table class="${cls}"`) : html
}
}

/** ```mermaid code blocks are rendered client side by the <Mermaid> component. */
function mermaid(md: MarkdownIt) {
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx]
if (token.info.trim() === 'mermaid') {
return `<Mermaid code="${encodeURIComponent(token.content)}" />`
}
return fence(tokens, idx, options, env, self)
}
}

export function configureMarkdown(md: MarkdownIt) {
const parse = md.parse.bind(md)
md.parse = (src, env) => {
const file = env?.realPath ?? env?.path
return parse(file ? expandPartials(src, file, env.includes) : src, env)
}
md.use(footnote)
md.use(canonicalLinks)
md.use(hardwareSpecs)
md.use(mermaid)
}
Loading
Loading