mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-12 20:45:52 +00:00
Update .gitignore to exclude generated chapter sync output, enhance package.json scripts for chapter synchronization, and update README and CONTRIBUTING.md for improved clarity on chapter structure and development processes. Remove obsolete components related to quadratic demos to streamline the project.
This commit is contained in:
parent
742a72abe2
commit
2ac34d52fc
23 changed files with 700 additions and 214 deletions
|
|
@ -74,7 +74,7 @@ bun run build # production: JS + Zola + pagefind → public/
|
|||
|
||||
- **Component UI** only (`pagefind-component-ui.js`), not legacy `pagefind-ui.js`.
|
||||
- Index runs **after** `zola build`. Production: `bun run build`. Dev: `bun run index:search` copies bundle to `static/pagefind/` (gitignored) for `zola serve`.
|
||||
- v1 indexes **live chapter pages only** (`data-pagefind-body` on `templates/math/chapter.html`); home, about, catalog, credits, and other site pages are excluded.
|
||||
- v1 indexes **live chapter pages only** (`data-pagefind-body` on `templates/chapter.html`); home, about, catalog, credits, and other site pages are excluded.
|
||||
|
||||
Detailed checklists: [curriculum-data.md](curriculum-data.md). Human-facing guide: [CONTRIBUTING.md](../../../CONTRIBUTING.md).
|
||||
|
||||
|
|
@ -105,16 +105,17 @@ Detailed checklists: [curriculum-data.md](curriculum-data.md). Human-facing guid
|
|||
|
||||
Prerequisites: **only** `graph.edges` (required, global DAG, cross-strand OK). Longest-path levels. No implicit strand-list chains. List order ≠ map order.
|
||||
|
||||
## Chapter content (current pattern)
|
||||
## Chapter content (colocated folder)
|
||||
|
||||
Live math example:
|
||||
Live chapters live under `content/{subject}/{slug}/`:
|
||||
|
||||
1. `content/math/quadratic-equations/_index.md` — front matter, `template = "math/chapter.html"`, `[extra] chapter_id`
|
||||
2. `templates/partials/math/quadratic-equations-core-content.html` — prose + KaTeX (supplement partial for history, derivation, etc.)
|
||||
3. `templates/math/chapter.html` — `{% if section.extra.chapter_id == "…" %}` includes partial
|
||||
4. Optional: `frontend/src/components/quadratic-explorer.tsx` + mount in `main.tsx`
|
||||
1. `data/{subject}-curriculum.json` — catalog metadata (`status`, `curriculums`, `graph.edges`)
|
||||
2. `_index.md` — front matter only (`template = "chapter.html"`, `[extra] subject`, `chapter_id`, `strand`)
|
||||
3. `core.html` (required), optional `supplement.html`, `assets/`, `widgets/*.tsx`, optional `chapter.scss`
|
||||
4. `bun run sync:chapters` merges HTML into `_index.md`, copies assets to `static/chapters/…`, generates `frontend/src/generated/chapter-widgets.ts`
|
||||
5. `templates/chapter.html` renders `{{ section.content | safe }}`; widgets use `data-widget` + auto-mount in `main.tsx`
|
||||
|
||||
**Pain point:** each new chapter needs a template `if` branch until a generic chapter loader exists.
|
||||
Reference: `content/math/quadratic-equations/`.
|
||||
|
||||
## Verify before PR
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ bun run build # build:js + zola build + index:search
|
|||
|
||||
| Source | Indexed? |
|
||||
|--------|----------|
|
||||
| `templates/math/chapter.html` → `<article data-pagefind-body>` | Yes |
|
||||
| `templates/chapter.html` → `<article data-pagefind-body>` | Yes |
|
||||
| Home, about, catalog, credits, contributing, `/search/` | No |
|
||||
| Catalog chapter cards (Solid + JSON) | No |
|
||||
| Planned chapters (no `content/` page) | No |
|
||||
|
|
@ -51,10 +51,10 @@ New subject chapter templates must include `data-pagefind-body` on the main less
|
|||
|
||||
### Per-chapter markup
|
||||
|
||||
[`themes/openfreebooks/templates/math/chapter.html`](../../../themes/openfreebooks/templates/math/chapter.html):
|
||||
[`themes/openfreebooks/templates/chapter.html`](../../../themes/openfreebooks/templates/chapter.html):
|
||||
|
||||
```html
|
||||
<article class="book-chapter" data-pagefind-body>
|
||||
<article class="book-chapter" data-pagefind-body data-chapter="math/quadratic-equations">
|
||||
…
|
||||
<div id="copy-page-button" … data-pagefind-ignore></div>
|
||||
<div id="quadratic-explorer" … data-pagefind-ignore></div>
|
||||
|
|
|
|||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -24,3 +24,9 @@ Thumbs.db
|
|||
|
||||
# Vite build output (bun run build:js → zola copies into public/)
|
||||
themes/openfreebooks/static/
|
||||
|
||||
# Chapter sync output (bun run sync:chapters)
|
||||
frontend/src/generated/
|
||||
data/_generated/
|
||||
static/css/chapters/
|
||||
static/chapters/
|
||||
|
|
|
|||
160
CONTRIBUTING.md
160
CONTRIBUTING.md
|
|
@ -20,32 +20,30 @@ Thank you for helping grow free, open textbooks.
|
|||
|
||||
**Browse the catalog locally:** `bun run build:js && zola serve` → [http://127.0.0.1:1111/catalog/](http://127.0.0.1:1111/catalog/)
|
||||
|
||||
**Search locally:** After `bun run build:js`, run `bun run index:search` once (or again after changing chapter HTML), then `zola serve`. Try [http://127.0.0.1:1111/search/](http://127.0.0.1:1111/search/) or press ⌘K / Ctrl+K. Pagefind indexes **live chapter pages only** (not home, about, catalog, or credits). Without `index:search`, the search UI loads but returns no results.
|
||||
**Search locally:** After `bun run build:js`, run `bun run index:search` once (or again after changing chapter HTML), then `zola serve`. Try [http://127.0.0.1:1111/search/](http://127.0.0.1:1111/search/) or press ⌘K / Ctrl+K.
|
||||
|
||||
---
|
||||
|
||||
## How the catalog fits together
|
||||
|
||||
```text
|
||||
data/catalog.json → subjects in the sidebar + filter chips (DSE, IB, …)
|
||||
data/{subject}-curriculum.json → strands, chapters, curriculum tags, graph.edges
|
||||
content/{subject}/{slug}/ → actual chapter page (when status is "live")
|
||||
themes/.../partials/... → chapter HTML content (today)
|
||||
data/{subject}-curriculum.json → catalog list + map (status, curriculums, graph.edges)
|
||||
content/{subject}/{slug}/ → everything for the live page (one folder)
|
||||
_index.md, core.html, supplement.html, assets/, widgets/, chapter.scss
|
||||
```
|
||||
|
||||
- **Catalog** (`/catalog/`) — **List** (numbered cards; live = whole card links) and **Map** (`?view=tree`, Mermaid; live = title link only with `→` and hover underline).
|
||||
- **`/math/`** redirects to `/catalog/?subject=math`.
|
||||
- **Curriculum names** (DSE, IB, …) appear only in the catalog — not inside chapter text ([spec.md](spec.md)).
|
||||
- **Catalog** (`/catalog/`) — list and map (`?view=tree`). Curriculum names (DSE, IB, …) appear only in the catalog — not inside chapter prose ([spec.md](spec.md)).
|
||||
- Every chapter needs a **`description`** in curriculum JSON (map clamps to three lines).
|
||||
- **`bun run sync:chapters`** (also run automatically by `bun run build:js`) merges `core.html` + `supplement.html` into `_index.md` for Zola. Edit the HTML files, not the merged body in `_index.md`.
|
||||
|
||||
### Map graph rules (summary)
|
||||
|
||||
| Rule | Detail |
|
||||
|------|--------|
|
||||
| Edges | **Only** `graph.edges` — no automatic chain from strand `chapters[]` order |
|
||||
| Edges | **Only** `graph.edges` — strand `chapters[]` order does not create arrows |
|
||||
| Meaning | Each edge is a **required** prerequisite (`from` → `to`) |
|
||||
| Scope | Cross-strand edges allowed |
|
||||
| Layout | **Longest-path** levels; siblings at the same level stack vertically in a column |
|
||||
| Layout | **Longest-path** levels; siblings stack vertically |
|
||||
| DAG | No cycles — build throws if the graph cycles |
|
||||
| `tier` | Optional `foundation` (default) or `non-foundation` (Extension badge) |
|
||||
|
||||
|
|
@ -53,11 +51,8 @@ themes/.../partials/... → chapter HTML content (today)
|
|||
|
||||
## Add a planned chapter (roadmap)
|
||||
|
||||
Use this when the chapter is not written yet but should appear in the catalog.
|
||||
|
||||
1. Open `data/{subject}-curriculum.json` (e.g. `data/math-curriculum.json`).
|
||||
2. Find the right **strand** (topic group).
|
||||
3. Add a chapter object:
|
||||
2. Add a chapter object under the right **strand**:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -70,136 +65,98 @@ Use this when the chapter is not written yet but should appear in the catalog.
|
|||
}
|
||||
```
|
||||
|
||||
(`tier` is optional — omit for foundation topics; `non-foundation` shows an **Extension** badge.)
|
||||
|
||||
4. Use a **kebab-case** `slug` (letters, numbers, hyphens only).
|
||||
5. Set `curriculums` to every syllabus tag that applies. Tags must exist in `data/catalog.json` → `curriculums`.
|
||||
6. Run `zola build` and check `/catalog/?subject=math` (or your subject).
|
||||
|
||||
No `content/` files needed for planned chapters.
|
||||
3. Use **kebab-case** slugs. No `content/` folder needed for planned chapters.
|
||||
|
||||
---
|
||||
|
||||
## Publish a live chapter
|
||||
|
||||
A live chapter needs **catalog data** and a **page**.
|
||||
|
||||
### 1. Catalog entry
|
||||
|
||||
In `data/{subject}-curriculum.json`, set `"status": "live"` and include a `description` (see [planned chapter](#add-a-planned-chapter-roadmap)).
|
||||
In `data/{subject}-curriculum.json`, set `"status": "live"` and include `description`, `curriculums`, and optional `tier`.
|
||||
|
||||
### 2. Zola section
|
||||
### 2. Chapter folder
|
||||
|
||||
Create `content/{subject}/{slug}/_index.md`:
|
||||
Create `content/{subject}/{slug}/`:
|
||||
|
||||
```text
|
||||
content/math/your-chapter-slug/
|
||||
├── _index.md # page metadata (see below)
|
||||
├── core.html # required — main lesson HTML
|
||||
├── supplement.html # optional — history, checkpoints, extra demos
|
||||
├── chapter.scss # optional — chapter-specific styles
|
||||
├── assets/ # optional — images (synced to static/chapters/…)
|
||||
└── widgets/ # optional — Solid widgets (auto-mounted)
|
||||
└── my-widget.tsx # default export; use data-widget="my-widget" in HTML
|
||||
```
|
||||
|
||||
**`_index.md` front matter** (page fields only — catalog fields stay in JSON):
|
||||
|
||||
```md
|
||||
+++
|
||||
title = "Your chapter title"
|
||||
description = "One sentence for search and previews — no curriculum names here."
|
||||
template = "math/chapter.html"
|
||||
template = "chapter.html"
|
||||
weight = 10
|
||||
[extra]
|
||||
subject = "math"
|
||||
chapter_id = "your-chapter-slug"
|
||||
strand = "Number & Algebra"
|
||||
+++
|
||||
```
|
||||
|
||||
- `slug` in the path must match `slug` in the JSON file.
|
||||
- `chapter_id` must match the slug (used by the template).
|
||||
- `weight` controls order within the section (lower = earlier).
|
||||
### 3. HTML and widgets
|
||||
|
||||
### 3. Chapter HTML content
|
||||
- Use shared classes: `book-prose`, `book-prose__heading`, `book-callout`, etc. Copy from `content/math/quadratic-equations/`.
|
||||
- **Do not** mention DSE, IB, or other curriculum names in chapter HTML.
|
||||
- Mount widgets with `<div class="math-widget-mount" data-widget="widget-name" data-pagefind-ignore></div>` (filename without `.tsx` = `data-widget` value).
|
||||
- Reference synced assets with root-relative paths: `/chapters/math/your-chapter-slug/image.png` (chapter HTML is not processed as Tera)
|
||||
|
||||
Today, chapter bodies live as HTML partials under the theme, not Markdown:
|
||||
### 4. Build and check
|
||||
|
||||
1. Add `themes/openfreebooks/templates/partials/math/your-chapter-slug-content.html` with the lesson HTML.
|
||||
2. In `themes/openfreebooks/templates/math/chapter.html`, include it when `chapter_id` matches (chapter content is inside `<article data-pagefind-body>` for search indexing):
|
||||
|
||||
```html
|
||||
{% if section.extra.chapter_id == "your-chapter-slug" %}
|
||||
{% include "partials/math/your-chapter-slug-content.html" %}
|
||||
{% endif %}
|
||||
```bash
|
||||
bun run build:js # sync:chapters + Vite bundle
|
||||
zola serve
|
||||
```
|
||||
|
||||
Use existing classes: `book-prose`, `book-prose__heading`, `book-callout`, etc. Copy from `quadratic-equations-core-content.html`.
|
||||
- `/catalog/?subject=math` — chapter appears and links correctly.
|
||||
- `/math/your-chapter-slug/` — page renders.
|
||||
- `bun run build` — full production build.
|
||||
|
||||
**Do not** mention DSE, IB, or other curriculum names in the chapter body.
|
||||
|
||||
### 4. Interactive widgets (optional)
|
||||
|
||||
If the chapter needs a Solid widget (like the quadratic explorer):
|
||||
|
||||
1. Add a component under `frontend/src/components/`.
|
||||
2. Mount it in `frontend/src/main.tsx` when a mount element exists.
|
||||
3. Add `<div id="your-widget" class="math-widget-mount"></div>` in the chapter template branch.
|
||||
|
||||
Then run `bun run build:js`.
|
||||
|
||||
### 5. Check
|
||||
|
||||
- `/catalog/?subject=math` — chapter appears; live chapters link correctly.
|
||||
- `/math/your-chapter-slug/` — page renders; breadcrumb goes to catalog.
|
||||
- `bun run build` completes without errors.
|
||||
No theme edits, no `main.tsx` edits, no per-chapter template branches.
|
||||
|
||||
---
|
||||
|
||||
## Chapter graph (branches)
|
||||
|
||||
The **Map** tab (`?view=tree`) shows chapters as a directed graph (strand = column, arrows = prerequisites).
|
||||
|
||||
- Prerequisites are **only** declared in `graph.edges` (strand list order does not create arrows).
|
||||
- For **branches** or **merges**, add edges:
|
||||
Add edges in `data/{subject}-curriculum.json`:
|
||||
|
||||
```json
|
||||
"graph": {
|
||||
"edges": [
|
||||
{ "from": "quadratic-equations", "to": "sequences-series" },
|
||||
{ "from": "quadratic-equations", "to": "functions-graphs" },
|
||||
{ "from": "functions-graphs", "to": "linear-programming" },
|
||||
{ "from": "sequences-series", "to": "linear-programming" }
|
||||
{ "from": "quadratic-equations", "to": "sequences-series" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `from` and `to` are chapter **slugs**.
|
||||
- The graph must **not contain cycles**.
|
||||
- Edges may cross strands.
|
||||
|
||||
Open `/catalog/?subject=math&view=tree` to verify. Use Ctrl/Cmd + wheel on the map to zoom.
|
||||
`from` / `to` are chapter **slugs**. Graph must be acyclic. Verify at `/catalog/?subject=math&view=tree`.
|
||||
|
||||
---
|
||||
|
||||
## Add a new subject
|
||||
|
||||
Example: adding **Science**.
|
||||
|
||||
1. **`data/catalog.json`** — add to `subjects`:
|
||||
|
||||
```json
|
||||
{ "id": "science", "name": "Science" }
|
||||
```
|
||||
|
||||
2. **`data/science-curriculum.json`** — create with `title`, `strands`, and chapters (copy structure from `math-curriculum.json`).
|
||||
|
||||
3. **`themes/openfreebooks/templates/catalog.html`** — wire the new file into `#catalog-data` (see `math` block).
|
||||
|
||||
4. **`content/science/`** — add `_index.md` when you have a section.
|
||||
|
||||
5. **Homepage** — optional card in `themes/openfreebooks/templates/index.html`.
|
||||
|
||||
6. Build and test `/catalog/?subject=science`.
|
||||
1. `data/catalog.json` — add to `subjects`.
|
||||
2. Create `data/{id}-curriculum.json` (copy structure from `math-curriculum.json`).
|
||||
3. Wire the subject in `themes/openfreebooks/templates/catalog.html` (until auto-wire lands).
|
||||
4. Add `content/{subject}/` when you have chapters.
|
||||
|
||||
---
|
||||
|
||||
## Add a curriculum label
|
||||
|
||||
Example: adding **GCSE**.
|
||||
|
||||
1. Add `"GCSE"` to `curriculums` in `data/catalog.json`.
|
||||
2. Tag chapters with `"GCSE"` in their `curriculums` arrays.
|
||||
3. Add a badge style in `themes/openfreebooks/sass/_catalog.scss` (copy `.catalog-badge--igcse` pattern).
|
||||
4. Add a mapping in `frontend/src/lib/catalog-badge.ts` → `curriculumBadgeClass()` (or rely on the default style).
|
||||
1. Add the label to `curriculums` in `data/catalog.json`.
|
||||
2. Tag chapters in their `curriculums` arrays.
|
||||
3. Add badge style in `themes/openfreebooks/sass/_catalog.scss` and mapping in `frontend/src/lib/catalog-badge.ts`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -207,22 +164,23 @@ Example: adding **GCSE**.
|
|||
|
||||
```bash
|
||||
bun install
|
||||
bun run build:js # after frontend/ changes
|
||||
bun run sync:chapters # after content/ chapter changes
|
||||
bun run build:js # sync + Vite
|
||||
zola serve
|
||||
bun test # catalog map generation (catalog-to-mermaid)
|
||||
bun test
|
||||
bun run build
|
||||
```
|
||||
|
||||
- Do **not** commit `themes/openfreebooks/static/` (Vite output). Run `bun run build:js` before `zola build` or `zola serve`.
|
||||
- Do **not** commit `themes/openfreebooks/static/` (Vite output).
|
||||
- Do **not** commit `public/`.
|
||||
|
||||
---
|
||||
|
||||
## Pull requests
|
||||
|
||||
- Keep changes focused (one subject or a few chapters per PR is fine).
|
||||
- Keep changes focused.
|
||||
- Run `bun run build` before opening the PR.
|
||||
- Describe what you added: subject, chapters, live vs planned, and which curricula apply.
|
||||
- Describe subject, chapters, live vs planned, and curricula.
|
||||
|
||||
Questions? Open a [GitHub discussion](https://github.com/hananoshikayomaru/openfreebooks) or issue.
|
||||
|
||||
|
|
@ -230,4 +188,4 @@ Questions? Open a [GitHub discussion](https://github.com/hananoshikayomaru/openf
|
|||
|
||||
## For maintainers & agents
|
||||
|
||||
Machine-oriented detail: [.agents/skills/ofb/SKILL.md](.agents/skills/ofb/SKILL.md) and [.agents/skills/ofb/curriculum-data.md](.agents/skills/ofb/curriculum-data.md).
|
||||
Machine-oriented detail: [.agents/skills/ofb/SKILL.md](.agents/skills/ofb/SKILL.md).
|
||||
|
|
|
|||
|
|
@ -103,9 +103,9 @@ These issues make contributions harder than they should be. PRs that fix any ite
|
|||
|
||||
### Chapters & content
|
||||
|
||||
- [ ] **Generic chapter template** — drop per-chapter `{% if chapter_id %}` branches; load `partials/{subject}/{slug}-content.html` by convention.
|
||||
- [ ] **Strand kicker from data** — chapter header still hardcodes “Number & Algebra”; should come from strand in curriculum JSON.
|
||||
- [ ] **Subject-agnostic chapter template** — breadcrumb/catalog URL and `load_data` path should use section path, not `math` only.
|
||||
- [x] **Colocated chapter folders** — `content/{subject}/{slug}/` with `sync:chapters`, generic `chapter.html`, auto widget registry.
|
||||
- [x] **Strand kicker from front matter** — `[extra] strand` in `_index.md`.
|
||||
- [ ] **Subject-agnostic catalog load in chapter template** — breadcrumb still loads `math-curriculum.json` literally until more subjects go live.
|
||||
- [ ] **Markdown chapters (optional)** — README/spec disagree on format; if staying HTML, document clearly everywhere.
|
||||
- [ ] **Defuddle “copy as Markdown”** — spec calls for it on every page; not implemented.
|
||||
|
||||
|
|
|
|||
47
bun.lock
47
bun.lock
|
|
@ -13,7 +13,8 @@
|
|||
"three": "^0.184.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"pagefind": "^1.3.0",
|
||||
"pagefind": "^1.5.0",
|
||||
"sass": "^1.89.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-solid": "^2.11.6",
|
||||
|
|
@ -147,6 +148,34 @@
|
|||
|
||||
"@pagefind/windows-x64": ["@pagefind/windows-x64@1.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="],
|
||||
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="],
|
||||
|
||||
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="],
|
||||
|
||||
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="],
|
||||
|
||||
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="],
|
||||
|
||||
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="],
|
||||
|
||||
"@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="],
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="],
|
||||
|
|
@ -289,6 +318,8 @@
|
|||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
|
@ -383,6 +414,8 @@
|
|||
|
||||
"delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
|
||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
|
@ -419,10 +452,16 @@
|
|||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"immutable": ["immutable@5.1.5", "", {}, "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A=="],
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
|
@ -455,6 +494,8 @@
|
|||
|
||||
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.44", "", {}, "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
|
@ -477,6 +518,8 @@
|
|||
|
||||
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
||||
|
||||
"rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="],
|
||||
|
|
@ -487,6 +530,8 @@
|
|||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"sass": ["sass@1.99.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="],
|
||||
|
|
|
|||
|
|
@ -1,8 +1,235 @@
|
|||
+++
|
||||
title = "Quadratic equations"
|
||||
description = "The discriminant, nature of roots, and the graph of y = ax² + bx + c."
|
||||
template = "math/chapter.html"
|
||||
template = "chapter.html"
|
||||
weight = 1
|
||||
[extra]
|
||||
subject = "math"
|
||||
chapter_id = "quadratic-equations"
|
||||
strand = "Number & Algebra"
|
||||
+++
|
||||
|
||||
<div class="book-prose">
|
||||
<p>
|
||||
A quadratic equation in one unknown has the form \(ax^2 + bx + c = 0\), where
|
||||
\(a \neq 0\). You can solve these by factorisation, the quadratic
|
||||
formula, or by reading information from the graph of \(y = ax^2 + bx + c\).
|
||||
</p>
|
||||
|
||||
<h2 id="discriminant" class="book-prose__heading">The discriminant</h2>
|
||||
<p>
|
||||
The <strong>discriminant</strong> \(\Delta\) (delta) is defined below. It tells you how many
|
||||
<em>real</em> roots the equation has without fully solving it.
|
||||
</p>
|
||||
|
||||
<div class="book-callout book-callout--formula" role="note">
|
||||
<p class="book-callout__label">Definition</p>
|
||||
<p class="book-formula">\[ \Delta = b^2 - 4ac \]</p>
|
||||
</div>
|
||||
|
||||
<h2 id="nature-of-roots" class="book-prose__heading">Nature of roots</h2>
|
||||
<div class="book-table-wrap">
|
||||
<table class="book-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Discriminant</th>
|
||||
<th scope="col">Nature of roots</th>
|
||||
<th scope="col">Graph of \(y = ax^2 + bx + c\)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>\(\Delta > 0\)</td>
|
||||
<td>Two distinct real roots</td>
|
||||
<td>Parabola cuts the x-axis twice</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>\(\Delta = 0\)</td>
|
||||
<td>One repeated real root (equal roots)</td>
|
||||
<td>Vertex touches the x-axis</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>\(\Delta < 0\)</td>
|
||||
<td>No real roots</td>
|
||||
<td>Parabola lies entirely above or below the x-axis</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
When \(\Delta \geq 0\), the quadratic formula gives the roots:
|
||||
\[ x = \frac{-b \pm \sqrt{\Delta}}{2a} \]
|
||||
</p>
|
||||
|
||||
<h2 id="try-it-yourself" class="book-prose__heading">Try it yourself</h2>
|
||||
<p>
|
||||
Use the explorer below to change \(a\), \(b\), and \(c\). Watch how \(\Delta\) and the
|
||||
parabola change, and read off the nature of the roots.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="math-widget-mount" data-widget="explorer" data-pagefind-ignore></div>
|
||||
|
||||
<div class="book-prose">
|
||||
<h2 id="history" class="book-prose__heading">History</h2>
|
||||
<p>
|
||||
People have worked on problems that lead to quadratic equations for thousands of years.
|
||||
Babylonian clay tablets from around 1800 BCE record area and length puzzles — for example,
|
||||
finding the sides of a rectangle when you know its area and perimeter. Those situations
|
||||
produce equations in one unknown whose highest power is two.
|
||||
</p>
|
||||
|
||||
<figure class="book-figure">
|
||||
<img
|
||||
class="book-figure__img"
|
||||
src="/chapters/math/quadratic-equations/babylonian-quadratic-tablet.png"
|
||||
alt="Babylonian clay tablet with cuneiform text recording a quadratic area problem"
|
||||
width="960"
|
||||
height="640"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<figcaption class="book-figure__caption">
|
||||
A Babylonian clay tablet (about 1800 BCE) with a problem equivalent to finding the
|
||||
sides of a rectangle from its area and perimeter.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>
|
||||
Early methods were often geometric: you imagine completing a rectangle to make a square,
|
||||
which is the same idea behind <em>completing the square</em> today. In the 9th century,
|
||||
<strong>al-Khwarizmi</strong> wrote systematic rules for many types of quadratic problems,
|
||||
sorted by which coefficients were present. He did not use modern symbols like \(x^2\), but
|
||||
the logic matches what we still teach.
|
||||
</p>
|
||||
<p>
|
||||
During the Renaissance, mathematicians such as <strong>Cardano</strong> and
|
||||
<strong>Tartaglia</strong> pushed further into solving cubics and quartics. Along the way they
|
||||
met equations whose discriminants were negative — no real roots, yet the algebra still
|
||||
demanded answers. That tension helped open the door to <strong>complex numbers</strong>, a
|
||||
topic you will meet in a later chapter.
|
||||
</p>
|
||||
|
||||
<h2 id="derivation" class="book-prose__heading">Derivation</h2>
|
||||
<p>
|
||||
The quadratic formula comes from rearranging \(ax^2 + bx + c = 0\) and completing the square.
|
||||
Below is a guided derivation; routine algebra between steps is left for you to check.
|
||||
</p>
|
||||
<p>
|
||||
Start with \(a \neq 0\) and divide every term by \(a\):
|
||||
\[ x^2 + \frac{b}{a}x + \frac{c}{a} = 0. \]
|
||||
Move the constant to the right:
|
||||
\[ x^2 + \frac{b}{a}x = -\frac{c}{a}. \]
|
||||
</p>
|
||||
<p>
|
||||
The expression \(x^2 + \frac{b}{a}x\) is almost a perfect square. Add and subtract
|
||||
\(\left(\frac{b}{2a}\right)^2\) on the left (equivalently, add that square to both sides):
|
||||
\[ x^2 + \frac{b}{a}x + \left(\frac{b}{2a}\right)^2 = -\frac{c}{a} + \left(\frac{b}{2a}\right)^2. \]
|
||||
The left side factors as a square:
|
||||
\[ \left(x + \frac{b}{2a}\right)^2 = \frac{b^2 - 4ac}{4a^2}. \]
|
||||
</p>
|
||||
<p>
|
||||
Take square roots (when the right-hand side is non-negative you get real solutions):
|
||||
\[ x + \frac{b}{2a} = \pm \frac{\sqrt{b^2 - 4ac}}{2a}. \]
|
||||
Solve for \(x\):
|
||||
</p>
|
||||
<div class="book-callout book-callout--formula" role="note">
|
||||
<p class="book-callout__label">Quadratic formula</p>
|
||||
<p class="book-formula">
|
||||
\[ x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} \]
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
The quantity under the square root is exactly the discriminant:
|
||||
\(\Delta = b^2 - 4ac\). So the formula is \(x = \frac{-b \pm \sqrt{\Delta}}{2a}\) whenever
|
||||
\(\Delta \geq 0\). If \(\Delta > 0\) you get two distinct real roots; if \(\Delta = 0\) the
|
||||
\(\pm\) gives the same root twice; if \(\Delta < 0\) there is no real square root and the
|
||||
equation has no real solutions — matching the nature-of-roots table above.
|
||||
</p>
|
||||
|
||||
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
|
||||
<p>
|
||||
Pause and think before moving on. There are no answers on this page — discuss with a
|
||||
classmate or write your reasoning in a notebook.
|
||||
</p>
|
||||
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Checkpoint</p>
|
||||
<p>
|
||||
In the explorer, keep \(a\) and \(b\) fixed and slowly change \(c\) so that \(\Delta\) goes
|
||||
from positive to zero. What happens to the two x-intercepts on the graph?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Checkpoint</p>
|
||||
<p>
|
||||
Can \(ax^2 + bx + c = 0\) with \(a \neq 0\) have three distinct real solutions? Why or why not?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Checkpoint</p>
|
||||
<p>
|
||||
Suppose \(a > 0\) and \(\Delta < 0\). Where does the entire parabola \(y = ax^2 + bx + c\)
|
||||
sit relative to the x-axis?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Checkpoint</p>
|
||||
<p>
|
||||
A ball is thrown straight upward from ground level. Without using numbers, describe a
|
||||
situation where \(\Delta > 0\) for height versus time, and another where \(\Delta = 0\).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Checkpoint</p>
|
||||
<p>
|
||||
After completing the square you had \(\left(x + \frac{b}{2a}\right)^2 = \frac{\Delta}{4a^2}\).
|
||||
Why must the sign of \(\Delta\) match whether the parabola crosses the x-axis?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 id="applications" class="book-prose__heading">Applications</h2>
|
||||
|
||||
<h3 id="projectile-motion" class="book-prose__heading">Projectile motion</h3>
|
||||
<p>
|
||||
Near Earth’s surface, height \(h\) (in metres) of an object thrown upward often follows
|
||||
\[ h = -\tfrac{1}{2}gt^2 + v_0 t + h_0, \]
|
||||
where \(t\) is time in seconds, \(g \approx 9.8\) is gravitational acceleration,
|
||||
\(v_0\) is initial upward speed, and \(h_0\) is starting height. This is a quadratic in
|
||||
\(t\). Setting \(h = 0\) asks when the object is at ground level; the solutions are the
|
||||
landing times. The discriminant tells you whether there are two crossing times
|
||||
(\(\Delta > 0\)), one (\(\Delta = 0\)), or none (\(\Delta < 0\)) — for example if the
|
||||
model never reaches the ground in the time you care about.
|
||||
</p>
|
||||
|
||||
<div class="math-widget-mount" data-widget="projectile-demo" data-pagefind-ignore></div>
|
||||
|
||||
<h3 id="profit-pricing" class="book-prose__heading">Profit and pricing</h3>
|
||||
<p>
|
||||
A shop might find that when it charges \(p\) dollars per item, revenue is \(p\) times
|
||||
quantity sold, while cost has a fixed part plus a cost per item. Profit (revenue minus cost)
|
||||
is often a quadratic in \(p\) that opens downward — there is a best price at the vertex.
|
||||
For example, with profit \(\Pi\) in dollars,
|
||||
\[ \Pi(p) = -2p^2 + 40p - 150. \]
|
||||
Setting \(\Pi = 0\) asks for <em>break-even</em> prices; \(\Delta\) tells you whether two
|
||||
break-even prices exist, one, or none.
|
||||
</p>
|
||||
|
||||
<div class="math-widget-mount" data-widget="profit-demo" data-pagefind-ignore></div>
|
||||
|
||||
<h3 id="area-fixed-perimeter" class="book-prose__heading">Area with fixed perimeter</h3>
|
||||
<p>
|
||||
A rectangle with perimeter \(P\) can be described by width \(x\) and length
|
||||
\(\frac{P}{2} - x\). Its area is
|
||||
\[ A(x) = x\left(\frac{P}{2} - x\right) = -\!x^2 + \frac{P}{2}x, \]
|
||||
a downward-opening parabola. The largest area occurs at the vertex, not at the extremes
|
||||
\(x = 0\) or \(x = \frac{P}{2}\) (which give zero area).
|
||||
</p>
|
||||
|
||||
<div class="math-widget-mount" data-widget="area-demo" data-pagefind-ignore></div>
|
||||
</div>
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 915 KiB |
|
|
@ -57,3 +57,5 @@
|
|||
parabola change, and read off the nature of the roots.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="math-widget-mount" data-widget="explorer" data-pagefind-ignore></div>
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
<figure class="book-figure">
|
||||
<img
|
||||
class="book-figure__img"
|
||||
src="{{ get_url(path='img/math/babylonian-quadratic-tablet.png', trailing_slash=false) | safe }}"
|
||||
src="/chapters/math/quadratic-equations/babylonian-quadratic-tablet.png"
|
||||
alt="Babylonian clay tablet with cuneiform text recording a quadratic area problem"
|
||||
width="960"
|
||||
height="640"
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
model never reaches the ground in the time you care about.
|
||||
</p>
|
||||
|
||||
<div id="projectile-demo" class="math-widget-mount" data-pagefind-ignore></div>
|
||||
<div class="math-widget-mount" data-widget="projectile-demo" data-pagefind-ignore></div>
|
||||
|
||||
<h3 id="profit-pricing" class="book-prose__heading">Profit and pricing</h3>
|
||||
<p>
|
||||
|
|
@ -147,7 +147,7 @@
|
|||
break-even prices exist, one, or none.
|
||||
</p>
|
||||
|
||||
<div id="profit-demo" class="math-widget-mount" data-pagefind-ignore></div>
|
||||
<div class="math-widget-mount" data-widget="profit-demo" data-pagefind-ignore></div>
|
||||
|
||||
<h3 id="area-fixed-perimeter" class="book-prose__heading">Area with fixed perimeter</h3>
|
||||
<p>
|
||||
|
|
@ -158,5 +158,5 @@
|
|||
\(x = 0\) or \(x = \frac{P}{2}\) (which give zero area).
|
||||
</p>
|
||||
|
||||
<div id="area-demo" class="math-widget-mount" data-pagefind-ignore></div>
|
||||
<div class="math-widget-mount" data-widget="area-demo" data-pagefind-ignore></div>
|
||||
</div>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import katex from "katex";
|
||||
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
|
||||
import { computeRoots, discriminant, formatNum } from "../lib/math-graph";
|
||||
import { computeRoots, discriminant, formatNum } from "../../../../frontend/src/lib/math-graph";
|
||||
|
||||
const HORIZONTAL_SPEED = 12;
|
||||
|
||||
1
content/math/quadratic-equations/widgets/area-demo.tsx
Normal file
1
content/math/quadratic-equations/widgets/area-demo.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { AreaDemo as default } from "./_application-demos";
|
||||
|
|
@ -340,7 +340,7 @@ function SliderRow(props: {
|
|||
);
|
||||
}
|
||||
|
||||
export function QuadraticExplorer() {
|
||||
function QuadraticExplorer() {
|
||||
const [a, setA] = createSignal(1);
|
||||
const [b, setB] = createSignal(0);
|
||||
const [c, setC] = createSignal(-4);
|
||||
|
|
@ -556,3 +556,5 @@ export function QuadraticExplorer() {
|
|||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuadraticExplorer;
|
||||
1
content/math/quadratic-equations/widgets/profit-demo.tsx
Normal file
1
content/math/quadratic-equations/widgets/profit-demo.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { ProfitDemo as default } from "./_application-demos";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { ProjectileDemo as default } from "./_application-demos";
|
||||
|
|
@ -36,6 +36,40 @@ function mountFooterYear() {
|
|||
}
|
||||
}
|
||||
|
||||
function mountChapterWidgets() {
|
||||
const article = document.querySelector<HTMLElement>("[data-chapter]");
|
||||
const chapterKey = article?.dataset.chapter;
|
||||
const mounts = document.querySelectorAll<HTMLElement>("[data-widget]");
|
||||
if (!chapterKey || mounts.length === 0) return;
|
||||
|
||||
void import("./generated/chapter-widgets").then(({ chapterWidgetLoaders }) => {
|
||||
const loadPromises: Promise<void>[] = [];
|
||||
|
||||
for (const el of mounts) {
|
||||
const widget = el.dataset.widget;
|
||||
if (!widget) continue;
|
||||
const loaderKey = `${chapterKey}/${widget}`;
|
||||
const loader = chapterWidgetLoaders[loaderKey];
|
||||
if (!loader) {
|
||||
console.warn(`No widget loader registered for ${loaderKey}`);
|
||||
continue;
|
||||
}
|
||||
loadPromises.push(
|
||||
loader().then((mod) => {
|
||||
render(() => mod.default(), el);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
void Promise.all(loadPromises).then(() => {
|
||||
if (!document.querySelector(".book-prose")) return;
|
||||
void import("./lib/render-math").then(({ renderBookMath }) => {
|
||||
requestAnimationFrame(() => renderBookMath());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initTheme();
|
||||
|
||||
const config = readConfig();
|
||||
|
|
@ -78,12 +112,6 @@ if (copyPageRoot) {
|
|||
});
|
||||
}
|
||||
|
||||
const hasQuadraticChapter =
|
||||
document.getElementById("quadratic-explorer") ||
|
||||
document.getElementById("projectile-demo") ||
|
||||
document.getElementById("profit-demo") ||
|
||||
document.getElementById("area-demo");
|
||||
|
||||
const chapterNavRoot = document.getElementById("chapter-heading-nav");
|
||||
if (chapterNavRoot) {
|
||||
void import("./components/chapter-heading-nav").then(({ ChapterHeadingNav }) => {
|
||||
|
|
@ -91,30 +119,7 @@ if (chapterNavRoot) {
|
|||
});
|
||||
}
|
||||
|
||||
if (hasQuadraticChapter) {
|
||||
void Promise.all([
|
||||
import("./components/quadratic-explorer"),
|
||||
import("./components/quadratic-application-demos"),
|
||||
import("./lib/render-math"),
|
||||
]).then(([{ QuadraticExplorer }, demos, { renderBookMath }]) => {
|
||||
const quadraticRoot = document.getElementById("quadratic-explorer");
|
||||
if (quadraticRoot) {
|
||||
render(() => <QuadraticExplorer />, quadraticRoot);
|
||||
}
|
||||
const appDemos: Array<[string, () => import("solid-js").JSX.Element]> = [
|
||||
["projectile-demo", demos.ProjectileDemo],
|
||||
["profit-demo", demos.ProfitDemo],
|
||||
["area-demo", demos.AreaDemo],
|
||||
];
|
||||
for (const [id, Demo] of appDemos) {
|
||||
const root = document.getElementById(id);
|
||||
if (root) {
|
||||
render(() => <Demo />, root);
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(() => renderBookMath());
|
||||
});
|
||||
}
|
||||
mountChapterWidgets();
|
||||
|
||||
const catalogRoot = document.getElementById("catalog-app");
|
||||
if (catalogRoot) {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:js": "vite build",
|
||||
"sync:chapters": "bun scripts/sync-chapters.ts",
|
||||
"build:js": "bun run sync:chapters && vite build",
|
||||
"build": "rm -f themes/openfreebooks/static/main.css public/main.css && bun run build:js && zola build && bun run index:search",
|
||||
"index:search": "pagefind --site public && rm -rf static/pagefind && cp -R public/pagefind static/pagefind",
|
||||
"dev:js": "vite build --watch",
|
||||
"dev:js": "bun run sync:chapters && vite build --watch",
|
||||
"scan:size": "bun scripts/scan-output-size.ts",
|
||||
"test": "bun test frontend/src/lib"
|
||||
},
|
||||
|
|
@ -20,6 +21,7 @@
|
|||
"three": "^0.184.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.89.2",
|
||||
"pagefind": "^1.5.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
|
|
|
|||
216
scripts/sync-chapters.ts
Normal file
216
scripts/sync-chapters.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readdir, readFile, rm, writeFile, copyFile } from "node:fs/promises";
|
||||
import { basename, dirname, join, relative } from "node:path";
|
||||
import { $ } from "bun";
|
||||
|
||||
const ROOT = join(import.meta.dir, "..");
|
||||
const CONTENT = join(ROOT, "content");
|
||||
const GENERATED_META = join(ROOT, "data/_generated/chapters-meta.json");
|
||||
const GENERATED_WIDGETS = join(ROOT, "frontend/src/generated/chapter-widgets.ts");
|
||||
const STATIC_ROOT = join(ROOT, "static");
|
||||
const CHAPTER_CSS_DIR = join(STATIC_ROOT, "css/chapters");
|
||||
|
||||
type ChapterMeta = {
|
||||
hasSupplement: boolean;
|
||||
hasCss: boolean;
|
||||
};
|
||||
|
||||
type ChaptersMetaFile = {
|
||||
chapters: Record<string, ChapterMeta>;
|
||||
};
|
||||
|
||||
function parseChapterTemplate(markdown: string): boolean {
|
||||
return /template\s*=\s*["']chapter\.html["']/.test(markdown);
|
||||
}
|
||||
|
||||
/** Chapter HTML is Zola section body, not a Tera template — rewrite legacy get_url tags. */
|
||||
function normalizeChapterHtml(html: string): string {
|
||||
return html
|
||||
.replace(
|
||||
/\{\{\s*get_url\(path=['"]([^'"]+)['"][^)]*\)\s*\|\s*safe\s*\}\}/g,
|
||||
(_, assetPath: string) => `/${assetPath.replace(/^\//, "")}`
|
||||
)
|
||||
.replace(/\{\{\s*get_url\([^}]+\)\s*\}\}/g, (tag) => {
|
||||
console.warn(`Unresolved Tera in chapter HTML (use /chapters/… paths): ${tag}`);
|
||||
return tag;
|
||||
});
|
||||
}
|
||||
|
||||
function splitFrontMatter(markdown: string): { frontMatter: string; body: string } {
|
||||
const match = markdown.match(/^(\+\+\+[\s\S]*?\+\+\+)\s*([\s\S]*)$/);
|
||||
if (!match) {
|
||||
return { frontMatter: markdown.trimEnd(), body: "" };
|
||||
}
|
||||
return { frontMatter: match[1], body: match[2].trim() };
|
||||
}
|
||||
|
||||
async function copyFileEnsuringDir(src: string, dest: string) {
|
||||
await mkdir(dirname(dest), { recursive: true });
|
||||
await copyFile(src, dest);
|
||||
}
|
||||
|
||||
async function copyDir(srcDir: string, destDir: string) {
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(srcDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const src = join(srcDir, entry.name);
|
||||
const dest = join(destDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await copyDir(src, dest);
|
||||
} else {
|
||||
await copyFileEnsuringDir(src, dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function compileChapterScss(
|
||||
subject: string,
|
||||
slug: string,
|
||||
scssPath: string
|
||||
): Promise<boolean> {
|
||||
const outPath = join(CHAPTER_CSS_DIR, `${subject}-${slug}.css`);
|
||||
await mkdir(CHAPTER_CSS_DIR, { recursive: true });
|
||||
const result = await $`sass ${scssPath} ${outPath} --style=compressed --no-source-map`.quiet();
|
||||
if (result.exitCode !== 0) {
|
||||
console.error(`Failed to compile ${scssPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function discoverChapters(): Promise<
|
||||
Array<{ subject: string; slug: string; dir: string }>
|
||||
> {
|
||||
const chapters: Array<{ subject: string; slug: string; dir: string }> = [];
|
||||
let subjects;
|
||||
try {
|
||||
subjects = await readdir(CONTENT, { withFileTypes: true });
|
||||
} catch {
|
||||
return chapters;
|
||||
}
|
||||
|
||||
for (const subjectEntry of subjects) {
|
||||
if (!subjectEntry.isDirectory() || subjectEntry.name.startsWith(".")) continue;
|
||||
const subject = subjectEntry.name;
|
||||
const subjectDir = join(CONTENT, subject);
|
||||
const slugEntries = await readdir(subjectDir, { withFileTypes: true });
|
||||
for (const slugEntry of slugEntries) {
|
||||
if (!slugEntry.isDirectory() || slugEntry.name.startsWith(".")) continue;
|
||||
const slug = slugEntry.name;
|
||||
const dir = join(subjectDir, slug);
|
||||
const indexPath = join(dir, "_index.md");
|
||||
if (!existsSync(indexPath)) continue;
|
||||
const markdown = await readFile(indexPath, "utf8");
|
||||
if (!parseChapterTemplate(markdown)) continue;
|
||||
chapters.push({ subject, slug, dir });
|
||||
}
|
||||
}
|
||||
return chapters;
|
||||
}
|
||||
|
||||
async function syncChapter(chapter: {
|
||||
subject: string;
|
||||
slug: string;
|
||||
dir: string;
|
||||
}): Promise<{ key: string; meta: ChapterMeta; widgets: string[] }> {
|
||||
const { subject, slug, dir } = chapter;
|
||||
const key = `${subject}/${slug}`;
|
||||
const corePath = join(dir, "core.html");
|
||||
if (!existsSync(corePath)) {
|
||||
console.error(`Missing core.html for chapter ${key}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const coreHtml = normalizeChapterHtml(await readFile(corePath, "utf8"));
|
||||
const supplementPath = join(dir, "supplement.html");
|
||||
const hasSupplement = existsSync(supplementPath);
|
||||
const supplementHtml = hasSupplement
|
||||
? normalizeChapterHtml(await readFile(supplementPath, "utf8"))
|
||||
: "";
|
||||
const mergedBody = hasSupplement
|
||||
? `${coreHtml.trim()}\n\n${supplementHtml.trim()}\n`
|
||||
: `${coreHtml.trim()}\n`;
|
||||
|
||||
const indexPath = join(dir, "_index.md");
|
||||
const indexRaw = await readFile(indexPath, "utf8");
|
||||
const { frontMatter } = splitFrontMatter(indexRaw);
|
||||
await writeFile(indexPath, `${frontMatter}\n\n${mergedBody}`);
|
||||
|
||||
const assetsDir = join(dir, "assets");
|
||||
const staticAssetsDir = join(STATIC_ROOT, "chapters", subject, slug);
|
||||
if (existsSync(assetsDir)) {
|
||||
await rm(staticAssetsDir, { recursive: true, force: true });
|
||||
await copyDir(assetsDir, staticAssetsDir);
|
||||
}
|
||||
|
||||
const scssPath = join(dir, "chapter.scss");
|
||||
let hasCss = false;
|
||||
if (existsSync(scssPath)) {
|
||||
hasCss = await compileChapterScss(subject, slug, scssPath);
|
||||
}
|
||||
|
||||
const widgetsDir = join(dir, "widgets");
|
||||
const widgets: string[] = [];
|
||||
if (existsSync(widgetsDir)) {
|
||||
const files = await readdir(widgetsDir);
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".tsx") && !file.startsWith("_")) {
|
||||
widgets.push(basename(file, ".tsx"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { key, meta: { hasSupplement, hasCss }, widgets };
|
||||
}
|
||||
|
||||
function buildWidgetRegistry(
|
||||
entries: Array<{ key: string; widgets: string[]; dir: string }>
|
||||
): string {
|
||||
const lines: string[] = [
|
||||
"// Auto-generated by scripts/sync-chapters.ts — do not edit.",
|
||||
"",
|
||||
"import type { Component } from \"solid-js\";",
|
||||
"",
|
||||
"export type ChapterWidgetModule = { default: Component };",
|
||||
"",
|
||||
"export const chapterWidgetLoaders: Record<string, () => Promise<ChapterWidgetModule>> = {",
|
||||
];
|
||||
|
||||
for (const { key, widgets, dir } of entries) {
|
||||
const relFromGenerated = relative(dirname(GENERATED_WIDGETS), dir).replace(/\\/g, "/");
|
||||
for (const widget of widgets) {
|
||||
const importPath = `${relFromGenerated}/widgets/${widget}.tsx`;
|
||||
lines.push(` "${key}/${widget}": () => import("${importPath}"),`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("};", "");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const chapters = await discoverChapters();
|
||||
const metaFile: ChaptersMetaFile = { chapters: {} };
|
||||
const widgetEntries: Array<{ key: string; widgets: string[]; dir: string }> = [];
|
||||
|
||||
for (const chapter of chapters) {
|
||||
const { key, meta, widgets } = await syncChapter(chapter);
|
||||
metaFile.chapters[key] = meta;
|
||||
widgetEntries.push({ key, widgets, dir: chapter.dir });
|
||||
console.log(`Synced chapter ${key} (${widgets.length} widget(s))`);
|
||||
}
|
||||
|
||||
await mkdir(dirname(GENERATED_META), { recursive: true });
|
||||
await writeFile(GENERATED_META, `${JSON.stringify(metaFile, null, 2)}\n`);
|
||||
|
||||
await mkdir(dirname(GENERATED_WIDGETS), { recursive: true });
|
||||
await writeFile(GENERATED_WIDGETS, buildWidgetRegistry(widgetEntries));
|
||||
|
||||
console.log(`Synced ${chapters.length} chapter(s).`);
|
||||
}
|
||||
|
||||
main();
|
||||
2
spec.md
2
spec.md
|
|
@ -22,7 +22,7 @@ we need to have a question bank feature. since this is purely static sites, ques
|
|||
4. choice
|
||||
5. ranking
|
||||
|
||||
question bank not only show answer but also steps and calcultion
|
||||
question bank not only show answer but also steps, explanation, calcultion.
|
||||
|
||||
## Chapters requirements
|
||||
|
||||
|
|
|
|||
63
themes/openfreebooks/templates/chapter.html
Normal file
63
themes/openfreebooks/templates/chapter.html
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% set subject_catalog = load_data(path="data/math-curriculum.json", format="json") %}
|
||||
{{ section.title }} | {{ subject_catalog.title }} | {{ config.extra.site_title }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block meta_description %}
|
||||
<meta name="description" content="{{ section.description | default(value=config.description) }}" />
|
||||
{% endblock meta_description %}
|
||||
|
||||
{% block extra_head %}
|
||||
{% set subject = section.extra.subject %}
|
||||
{% set slug = section.extra.chapter_id | default(value=section.slug) %}
|
||||
{% set chapter_key = subject ~ "/" ~ slug %}
|
||||
{% set chapters_meta = load_data(path="data/_generated/chapters-meta.json", format="json") %}
|
||||
{% set chapter_meta = chapters_meta.chapters[chapter_key] %}
|
||||
<link rel="stylesheet" href="{{ get_url(path='js/katex.css', trailing_slash=false) | safe }}" />
|
||||
{% if chapter_meta.hasCss %}
|
||||
<link rel="stylesheet" href="{{ get_url(path='css/chapters/' ~ subject ~ '-' ~ slug ~ '.css', trailing_slash=false) | safe }}" />
|
||||
{% endif %}
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
{% set subject = section.extra.subject %}
|
||||
{% set slug = section.extra.chapter_id | default(value=section.slug) %}
|
||||
{% set chapter_key = subject ~ "/" ~ slug %}
|
||||
{% set subject_catalog = load_data(path="data/math-curriculum.json", format="json") %}
|
||||
{% set catalog_subject_url = get_url(path="/catalog/") ~ "?subject=" ~ subject %}
|
||||
<section class="book-page book-page--chapter">
|
||||
<div class="container">
|
||||
<nav class="book-breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="{{ get_url(path='/') | safe }}">Home</a>
|
||||
<span class="book-breadcrumb__sep" aria-hidden="true">/</span>
|
||||
<a href="{{ catalog_subject_url | safe }}">{{ subject_catalog.title }}</a>
|
||||
<span class="book-breadcrumb__sep" aria-hidden="true">/</span>
|
||||
<span aria-current="page">{{ section.title }}</span>
|
||||
</nav>
|
||||
|
||||
<article
|
||||
class="book-chapter"
|
||||
data-pagefind-body
|
||||
data-chapter="{{ chapter_key }}"
|
||||
>
|
||||
<header class="book-chapter__header">
|
||||
<div class="book-chapter__header-main">
|
||||
{% if section.extra.strand %}
|
||||
<p class="section-kicker">{{ section.extra.strand }}</p>
|
||||
{% endif %}
|
||||
<h1 class="section-title">{{ section.title }}</h1>
|
||||
{% if section.description %}
|
||||
<p class="book-chapter__lead">{{ section.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="copy-page-button" class="book-chapter__actions" data-pagefind-ignore></div>
|
||||
</header>
|
||||
|
||||
<div id="chapter-heading-nav" data-pagefind-ignore></div>
|
||||
{{ section.content | safe }}
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ section.title }} | Mathematics | {{ config.extra.site_title }}{% endblock title %}
|
||||
{% block meta_description %}
|
||||
<meta name="description" content="{{ section.description | default(value=config.description) }}" />
|
||||
{% endblock meta_description %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="{{ get_url(path='js/katex.css', trailing_slash=false) | safe }}" />
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
{% set math_catalog = load_data(path="data/math-curriculum.json", format="json") %}
|
||||
{% set catalog_math_url = get_url(path="/catalog/") ~ "?subject=math" %}
|
||||
<section class="book-page book-page--chapter">
|
||||
<div class="container">
|
||||
<nav class="book-breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="{{ get_url(path='/') | safe }}">Home</a>
|
||||
<span class="book-breadcrumb__sep" aria-hidden="true">/</span>
|
||||
<a href="{{ catalog_math_url | safe }}">{{ math_catalog.title }}</a>
|
||||
<span class="book-breadcrumb__sep" aria-hidden="true">/</span>
|
||||
<span aria-current="page">{{ section.title }}</span>
|
||||
</nav>
|
||||
|
||||
<article class="book-chapter" data-pagefind-body>
|
||||
<header class="book-chapter__header">
|
||||
<div class="book-chapter__header-main">
|
||||
<p class="section-kicker">Number & Algebra</p>
|
||||
<h1 class="section-title">{{ section.title }}</h1>
|
||||
{% if section.description %}
|
||||
<p class="book-chapter__lead">{{ section.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="copy-page-button" class="book-chapter__actions" data-pagefind-ignore></div>
|
||||
</header>
|
||||
|
||||
{% if section.extra.chapter_id == "quadratic-equations" %}
|
||||
<div id="chapter-heading-nav" data-pagefind-ignore></div>
|
||||
{% include "partials/math/quadratic-equations-core-content.html" %}
|
||||
<div id="quadratic-explorer" class="math-widget-mount" data-pagefind-ignore></div>
|
||||
{% include "partials/math/quadratic-equations-supplement-content.html" %}
|
||||
{% endif %}
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
|
@ -28,10 +28,9 @@
|
|||
<section id="overview" class="contributing-section">
|
||||
<h2 class="contributing-section__title">How the catalog fits together</h2>
|
||||
<div class="contributing-prose">
|
||||
<pre class="contributing-code-block"><code>data/catalog.json → subjects + filter chips (DSE, IB, …)
|
||||
data/{subject}-curriculum.json → strands, chapters, graph.edges
|
||||
content/{subject}/{slug}/ → chapter page when status is "live"
|
||||
themes/.../partials/... → chapter HTML bodies (today)</code></pre>
|
||||
<pre class="contributing-code-block"><code>data/{subject}-curriculum.json → catalog + graph (status, curriculums, edges)
|
||||
content/{subject}/{slug}/ → live page (one folder)
|
||||
_index.md, core.html, supplement.html, assets/, widgets/</code></pre>
|
||||
<ul>
|
||||
<li><strong>List</strong> — numbered cards; live chapters link from the whole card.</li>
|
||||
<li><strong>Map</strong> (<code>?view=tree</code>) — Mermaid map with strand columns; live chapters link from the <strong>title</strong> only (<code>→</code> + hover underline).</li>
|
||||
|
|
@ -67,15 +66,13 @@ themes/.../partials/... → chapter HTML bodies (today)</code></pre>
|
|||
<section id="live-chapter" class="contributing-section">
|
||||
<h2 class="contributing-section__title">Publish a live chapter</h2>
|
||||
<div class="contributing-prose">
|
||||
<p>A live chapter needs catalog JSON <strong>and</strong> a Zola section.</p>
|
||||
<p>A live chapter needs a catalog JSON entry <strong>and</strong> a folder under <code>content/{subject}/{slug}/</code>.</p>
|
||||
<h3 class="contributing-section__subtitle">1. Catalog entry</h3>
|
||||
<p>Set <code>"status": "live"</code> and include <code>description</code> in the curriculum JSON.</p>
|
||||
<h3 class="contributing-section__subtitle">2. Zola section</h3>
|
||||
<p>Create <code>content/{subject}/{slug}/_index.md</code> with <code>chapter_id</code> matching the slug.</p>
|
||||
<h3 class="contributing-section__subtitle">3. Chapter HTML</h3>
|
||||
<p>Add <code>themes/openfreebooks/templates/partials/math/your-slug-content.html</code> and wire it in <code>math/chapter.html</code>. Copy patterns from <code>quadratic-equations-core-content.html</code>.</p>
|
||||
<h3 class="contributing-section__subtitle">4. Optional widget</h3>
|
||||
<p>Add a Solid component under <code>frontend/src/components/</code> and mount it in <code>main.tsx</code>. Run <code>bun run build:js</code>.</p>
|
||||
<p>Set <code>"status": "live"</code> in <code>data/{subject}-curriculum.json</code> with <code>description</code> and <code>curriculums</code>.</p>
|
||||
<h3 class="contributing-section__subtitle">2. Chapter folder</h3>
|
||||
<p>Add <code>_index.md</code>, <code>core.html</code>, optional <code>supplement.html</code>, <code>assets/</code>, <code>widgets/*.tsx</code>. Copy <code>content/math/quadratic-equations/</code>.</p>
|
||||
<h3 class="contributing-section__subtitle">3. Build</h3>
|
||||
<p><code>bun run build:js</code> syncs HTML and bundles widgets — no theme or <code>main.tsx</code> edits.</p>
|
||||
</div>
|
||||
|
||||
<aside class="contributing-card-demo" aria-label="Map chapter card example">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@ import solid from "vite-plugin-solid";
|
|||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
server: {
|
||||
fs: {
|
||||
allow: [".."],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: "themes/openfreebooks/static",
|
||||
emptyOutDir: false,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue