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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/docs-style.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Docs style

on:
pull_request:
paths:
- '**/*.mdx'
- '.vale.ini'
- 'styles/**'
- 'scripts/lint-docs.py'
workflow_dispatch:
inputs:
scope:
description: 'Files to lint'
type: choice
default: changed
options: [changed, all]

env:
VALE_VERSION: 3.9.6

jobs:
style:
name: STYLE.md
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

# Pages written before the linter existed still contain violations, so a
# pull request is judged only on the files it touches. Run the workflow
# manually with scope=all to see the full backlog.
- name: Collect files to lint
id: files
run: |
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ inputs.scope }}" != "all" ]; then
git diff --name-only --diff-filter=d \
"origin/${{ github.base_ref }}...HEAD" -- '*.mdx' > files.txt
else
git ls-files '*.mdx' > files.txt
fi
count=$(wc -l < files.txt | tr -d ' ')
echo "count=$count" >> "$GITHUB_OUTPUT"
echo "Linting $count file(s):"
cat files.txt

- name: Install Vale
if: steps.files.outputs.count != '0'
run: |
curl -sSL "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/vale_${VALE_VERSION}_Linux_64-bit.tar.gz" \
| tar -xz -C /usr/local/bin vale
vale --version

# Frontmatter, heading hierarchy, code fences, link paths, alt text.
- name: Structure
if: steps.files.outputs.count != '0'
run: xargs -a files.txt python3 scripts/lint-docs.py

# Voice, terminology, punctuation. Errors fail the job, warnings do not.
- name: Prose
if: always() && steps.files.outputs.count != '0'
run: xargs -a files.txt vale
16 changes: 16 additions & 0 deletions .vale.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
StylesPath = styles
MinAlertLevel = suggestion

Vocab = Fhenix

[formats]
mdx = md

# Only documentation pages are linted. STYLE.md and README.md are exempt:
# the style guide quotes the words it bans.
[*.mdx]
BasedOnStyles = Fhenix

# Frontmatter, JSX props, and fenced code blocks are not prose.
TokenIgnores = (\{[^}]*\})
BlockIgnores = (?s) *```.*?```, (?s)^---\n.*?\n---
113 changes: 113 additions & 0 deletions scripts/lint-docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Structural checks for Mintlify MDX pages.

Vale covers prose. This script covers the things Vale cannot see: frontmatter,
heading hierarchy, code fences, link paths, and alt text. It is fence-aware, so
nothing inside a code block is treated as prose or markup.

Usage:
python3 scripts/lint-docs.py [files...] # defaults to every .mdx in the repo

Exits 1 if any error is found. See STYLE.md.
"""

import re
import sys
from pathlib import Path

GUIDE = "STYLE.md"


def parse(path: Path):
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
front, body_start = {}, 0
if lines and lines[0].strip() == "---":
for i in range(1, len(lines)):
if lines[i].strip() == "---":
body_start = i + 1
break
m = re.match(r'\s*([A-Za-z0-9_-]+)\s*:\s*(.*)', lines[i])
if m:
front[m.group(1)] = m.group(2).strip().strip('"\'')
return lines, front, body_start


def check(path: Path):
lines, front, body_start = parse(path)
errors = []

def err(line_no, rule, message):
errors.append((path, line_no, rule, message))

# Snippets are fragments included into pages, so they carry no frontmatter.
is_snippet = "snippets" in path.parts
if not is_snippet:
if not front.get("title"):
err(1, "frontmatter", "Missing frontmatter `title`.")
if not front.get("description"):
err(1, "frontmatter", "Missing frontmatter `description`. One sentence saying what the page covers.")

in_fence, fence_marker, fence_line = False, "", 0
last_level = 1 # the H1 Mintlify renders from the frontmatter title

for i in range(body_start, len(lines)):
line = lines[i]
n = i + 1
stripped = line.strip()

fence = re.match(r'^(```+|~~~+)(.*)$', stripped)
if fence:
marker, info = fence.group(1), fence.group(2).strip()
if not in_fence:
in_fence, fence_marker, fence_line = True, marker[:3], n
if not info:
err(n, "code-lang", "Code block has no language tag. Add one, plus a filename where the reader needs to know where the code goes.")
elif marker.startswith(fence_marker):
in_fence = False
continue
if in_fence:
continue

heading = re.match(r'^(#{1,6})\s+(\S.*)$', line)
if heading:
level, title = len(heading.group(1)), heading.group(2)
if level == 1:
err(n, "manual-h1", "Manual H1. Mintlify renders the page H1 from the frontmatter `title`. Body headings start at `##`.")
elif level > last_level + 1:
err(n, "heading-skip", f"Heading level jumps from h{last_level} to h{level}. Do not skip levels.")
last_level = max(level, 2) if level > 1 else last_level
continue

for match in re.finditer(r'\]\((\.\.?/[^)\s]*)\)', line):
err(n, "link-path", f"Relative link `{match.group(1)}`. Use a root-relative path, such as `/fhe-library/core-concepts/access-control`.")
for match in re.finditer(r'\]\((https?://(?:cofhe-)?docs\.fhenix\.zone[^)\s]*)\)', line):
err(n, "link-path", f"Absolute link to our own site `{match.group(1)}`. Use a root-relative path so preview deployments resolve.")
if re.search(r'!\[\s*\]\(', line):
err(n, "alt-text", "Image has no alt text. Describe what the image shows.")
for match in re.finditer(r'<img\b[^>]*>', line):
if 'alt=' not in match.group(0):
err(n, "alt-text", "`<img>` has no `alt` attribute.")

if in_fence:
err(fence_line, "code-fence", "Code block is never closed.")

return errors


def main(argv):
paths = [Path(a) for a in argv[1:]] or sorted(Path(".").rglob("*.mdx"))
paths = [p for p in paths if p.suffix == ".mdx" and p.exists()]
errors = []
for path in paths:
errors.extend(check(path))

for path, line_no, rule, message in errors:
print(f"{path}:{line_no}: error [{rule}] {message}")

print(f"\n{len(errors)} error(s) in {len(paths)} file(s). See {GUIDE}.", file=sys.stderr)
return 1 if errors else 0


if __name__ == "__main__":
sys.exit(main(sys.argv))
15 changes: 15 additions & 0 deletions styles/Fhenix/Condescending.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
extends: existence
message: "Avoid '%s'. If the reader is stuck, the word is an insult; if not, it is noise."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#words
level: warning
ignorecase: true
tokens:
- 'simply'
- 'simple'
- 'just'
- 'easy'
- 'easily'
- 'obviously'
- 'of course'
- 'trivial(ly)?'
- 'straightforward'
17 changes: 17 additions & 0 deletions styles/Fhenix/DecorativeUnicode.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
extends: existence
message: "No decorative Unicode ('%s'). Use words: 'Shipped', 'In progress', 'Planned'."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#punctuation-and-symbols
level: error
nonword: true
tokens:
- '→'
- '←'
- '⇒'
- '✅'
- '❌'
- '✔'
- '✖'
- '✨'
- '🚀'
- '🎉'
- '🔥'
8 changes: 8 additions & 0 deletions styles/Fhenix/EmDash.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
extends: existence
message: "No em dashes. Rewrite with a comma, a period, a colon, or parentheses."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#punctuation-and-symbols
level: error
nonword: true
tokens:
- '—'
- ' -- '
7 changes: 7 additions & 0 deletions styles/Fhenix/Exclamation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
extends: existence
message: "Exclamation mark. At most one per tab, preferably zero."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#punctuation-and-symbols
level: warning
nonword: true
tokens:
- '\w!(\s|$)'
15 changes: 15 additions & 0 deletions styles/Fhenix/Filler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
extends: existence
message: "Filler phrase: '%s'. Cut it or say the thing directly."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#words
level: error
ignorecase: true
tokens:
- 'in order to'
- 'it is important to note that'
- "it's important to note that"
- 'it is worth noting that'
- "it's worth noting that"
- 'please note that'
- 'needless to say'
- 'as you can see'
- 'basically'
7 changes: 7 additions & 0 deletions styles/Fhenix/FutureTense.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
extends: existence
message: "Present tense for how the system works: '%s'. Planned behavior belongs on the future plans page, labeled as planned."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#voice
level: suggestion
ignorecase: true
tokens:
- 'will (be able to )?(send|return|compute|verify|decrypt|encrypt|queue|store|emit|call|allow)'
36 changes: 36 additions & 0 deletions styles/Fhenix/Headings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
extends: capitalization
message: "Headings use sentence case: '%s'."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#page-structure
level: warning
scope: heading
match: $sentence
indicators:
- ':'
exceptions:
- CoFHE
- FHE
- FheOS
- Teecryptor
- SDK
- API
- ACL
- TEE
- ZK
- MPC
- EVM
- ERC
- ERC20
- FHERC20
- Solidity
- Hardhat
- Foundry
- Fhenix
- TaskManager
- CommitmentRegistry
- Threshold Network
- JavaScript
- TypeScript
- React
- Node
- GitHub
- I
14 changes: 14 additions & 0 deletions styles/Fhenix/Idioms.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
extends: existence
message: "Idiom: '%s'. Say what happens instead. Idioms cost non-native readers a lookup."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#words
level: warning
ignorecase: true
tokens:
- 'out of the box'
- 'under the hood'
- 'a piece of cake'
- 'in a nutshell'
- 'the sky is the limit'
- 'hit the ground running'
- 'low-hanging fruit'
- 'bread and butter'
11 changes: 11 additions & 0 deletions styles/Fhenix/InternalNames.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
extends: existence
message: "Possible internal name: '%s'. Internal hosts, projects, environments, and flags do not belong in public docs."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#terminology
level: warning
ignorecase: true
tokens:
- 'rehearsal'
- 'staging'
- 'aggregator'
- '[a-z0-9-]+\.internal'
- 'gcp project'
8 changes: 8 additions & 0 deletions styles/Fhenix/LinkText.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
extends: existence
message: "Vague link text: '%s'. Describe the destination."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#links-images-and-components
level: warning
scope: raw
ignorecase: true
raw:
- '\[(click here|here|this link|this page|read more|learn more|link)\]\('
18 changes: 18 additions & 0 deletions styles/Fhenix/Marketing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
extends: existence
message: "Marketing adjective: '%s'. Describe the actual behavior instead."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#words
level: error
ignorecase: true
tokens:
- 'seamless(ly)?'
- 'blazing(ly)?'
- 'cutting-edge'
- 'state-of-the-art'
- 'best-in-class'
- 'game-chang(ing|er)'
- 'revolutionary'
- 'effortless(ly)?'
- 'lightning[- ]fast'
- 'world-class'
- 'robust'
- 'powerful'
8 changes: 8 additions & 0 deletions styles/Fhenix/SentenceLength.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
extends: occurrence
message: "Sentence is over 30 words. Aim for under 25. Split it."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#sentences-and-paragraphs
level: warning
scope: sentence
ignorecase: false
max: 30
token: '\b(\w+)\b'
10 changes: 10 additions & 0 deletions styles/Fhenix/SmartQuotes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
extends: existence
message: "Use straight quotes and apostrophes, not '%s'."
link: https://github.com/FhenixProtocol/fhenix-developer-docs/blob/main/STYLE.md#punctuation-and-symbols
level: warning
nonword: true
tokens:
- '“'
- '”'
- '‘'
- '’'
Loading
Loading