@@ -1,37 +1,25 @@ | |||||
import { defineConfig } from 'astro/config'; | import { defineConfig } from 'astro/config'; | ||||
import tailwind from '@astrojs/tailwind'; | import tailwind from '@astrojs/tailwind'; | ||||
import mdx from '@astrojs/mdx'; | import mdx from '@astrojs/mdx'; | ||||
import AutoImport from 'astro-auto-import'; | |||||
const defaultLayoutPlugin = () => (tree, file) => { | |||||
const path = file.history.at(-1).split('/').at(-1); | |||||
file.data.astro.frontmatter.layout = ( | |||||
path.startsWith('index.') | |||||
? '../layouts/Cover.astro' | |||||
: '../layouts/Default.astro' | |||||
); | |||||
}; | |||||
import autoImport from 'astro-auto-import'; | |||||
import react from '@astrojs/react'; | |||||
// https://astro.build/config | |||||
export default defineConfig({ | export default defineConfig({ | ||||
trailingSlash: 'never', | trailingSlash: 'never', | ||||
output: 'static', | output: 'static', | ||||
build: { | build: { | ||||
format: 'file', | |||||
format: 'file' | |||||
}, | }, | ||||
compressHTML: false, | compressHTML: false, | ||||
markdown: { | |||||
remarkPlugins: [defaultLayoutPlugin], | |||||
extendDefaultPlugins: true, | |||||
}, | |||||
integrations: [ | integrations: [ | ||||
tailwind({ | tailwind({ | ||||
applyBaseStyles: false, | |||||
applyBaseStyles: false | |||||
}), | }), | ||||
AutoImport({ | |||||
imports: [ | |||||
'./src/components/Score.astro', | |||||
], | |||||
autoImport({ | |||||
imports: ['./src/components/Score.astro'] | |||||
}), | }), | ||||
mdx() | |||||
mdx(), | |||||
react() | |||||
], | ], | ||||
}); | }); |
@@ -16,12 +16,12 @@ | |||||
"@astrojs/react": "^3.3.1", | "@astrojs/react": "^3.3.1", | ||||
"@astrojs/tailwind": "^5.1.0", | "@astrojs/tailwind": "^5.1.0", | ||||
"@theoryofnekomata/react-musical-keyboard": "1.0.13", | "@theoryofnekomata/react-musical-keyboard": "1.0.13", | ||||
"@types/react": "^18.3.0", | |||||
"@types/react": "^18.3.1", | |||||
"@types/react-dom": "^18.3.0", | "@types/react-dom": "^18.3.0", | ||||
"astro": "^4.5.16", | "astro": "^4.5.16", | ||||
"jsdom": "^24.0.0", | "jsdom": "^24.0.0", | ||||
"react": "^18.3.0", | |||||
"react-dom": "^18.3.0", | |||||
"react": "^18.3.1", | |||||
"react-dom": "^18.3.1", | |||||
"tailwindcss": "^3.4.3", | "tailwindcss": "^3.4.3", | ||||
"typescript": "^5.4.4", | "typescript": "^5.4.4", | ||||
"verovio": "^4.1.0" | "verovio": "^4.1.0" | ||||
@@ -33,7 +33,7 @@ | |||||
"@types/verovio": "^3.13.4", | "@types/verovio": "^3.13.4", | ||||
"archiver": "^7.0.1", | "archiver": "^7.0.1", | ||||
"astro-auto-import": "^0.4.2", | "astro-auto-import": "^0.4.2", | ||||
"tsx": "^4.7.2", | |||||
"prop-types": "^15.8.1" | |||||
"prop-types": "^15.8.1", | |||||
"tsx": "^4.7.2" | |||||
} | } | ||||
} | } |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"title": "Book Name" | |||||
} |
@@ -21,7 +21,7 @@ dependencies: | |||||
specifier: 1.0.13 | specifier: 1.0.13 | ||||
version: 1.0.13(mem@6.1.1)(react@18.3.1) | version: 1.0.13(mem@6.1.1)(react@18.3.1) | ||||
'@types/react': | '@types/react': | ||||
specifier: ^18.3.0 | |||||
specifier: ^18.3.1 | |||||
version: 18.3.1 | version: 18.3.1 | ||||
'@types/react-dom': | '@types/react-dom': | ||||
specifier: ^18.3.0 | specifier: ^18.3.0 | ||||
@@ -33,10 +33,10 @@ dependencies: | |||||
specifier: ^24.0.0 | specifier: ^24.0.0 | ||||
version: 24.0.0 | version: 24.0.0 | ||||
react: | react: | ||||
specifier: ^18.3.0 | |||||
specifier: ^18.3.1 | |||||
version: 18.3.1 | version: 18.3.1 | ||||
react-dom: | react-dom: | ||||
specifier: ^18.3.0 | |||||
specifier: ^18.3.1 | |||||
version: 18.3.1(react@18.3.1) | version: 18.3.1(react@18.3.1) | ||||
tailwindcss: | tailwindcss: | ||||
specifier: ^3.4.3 | specifier: ^3.4.3 | ||||
@@ -163,7 +163,7 @@ export const FrequenciesForm = () => { | |||||
return ( | return ( | ||||
<div> | <div> | ||||
{showForm && ( | {showForm && ( | ||||
<> | |||||
<div className="print-hidden"> | |||||
<form> | <form> | ||||
<input | <input | ||||
type="number" | type="number" | ||||
@@ -197,6 +197,7 @@ export const FrequenciesForm = () => { | |||||
/> | /> | ||||
</form> | </form> | ||||
<div style={{ position: 'relative', backgroundColor: 'black', }}> | <div style={{ position: 'relative', backgroundColor: 'black', }}> | ||||
{/* @ts-ignore */} | |||||
<MusicalKeyboard | <MusicalKeyboard | ||||
hasMap | hasMap | ||||
startKey={0} | startKey={0} | ||||
@@ -214,17 +215,20 @@ export const FrequenciesForm = () => { | |||||
/> | /> | ||||
</MusicalKeyboard> | </MusicalKeyboard> | ||||
</div> | </div> | ||||
</> | |||||
</div> | |||||
)} | )} | ||||
<table | <table | ||||
className="alternate-rows" | |||||
style={{ | style={{ | ||||
fontSize: '0.875em', | |||||
fontSize: '0.75em', | |||||
}} | }} | ||||
> | > | ||||
<caption> | <caption> | ||||
MIDI note frequencies and their stretched counterparts | MIDI note frequencies and their stretched counterparts | ||||
(base frequency={baseFrequency} Hz for key #{baseKey}, factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}) | |||||
(base frequency={baseFrequency} Hz for key #{baseKey}, stretch factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}). | |||||
First figures indicate non-stretched frequencies, second figures indicate stretched frequencies. Parenthesized figures represent difference | |||||
between the frequencies. | |||||
</caption> | </caption> | ||||
<thead> | <thead> | ||||
<tr> | <tr> | ||||
@@ -279,7 +283,7 @@ export const FrequenciesForm = () => { | |||||
<table> | <table> | ||||
<caption> | <caption> | ||||
MIDI note frequencies and their stretched counterparts | MIDI note frequencies and their stretched counterparts | ||||
(base frequency={baseFrequency} Hz for key #{baseKey}, factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}) | |||||
(base frequency={baseFrequency} Hz for key #{baseKey}, stretch factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}) | |||||
</caption> | </caption> | ||||
<thead> | <thead> | ||||
<tr> | <tr> | ||||
@@ -7,7 +7,7 @@ const scoreXml = await loadScore(id); | |||||
--- | --- | ||||
<div class="score-wrapper"> | <div class="score-wrapper"> | ||||
<a href={`scores/${id}.svg`}> | |||||
<a href={`../scores/${id}.svg`}> | |||||
<figure> | <figure> | ||||
<Fragment set:html={scoreXml} /> | <Fragment set:html={scoreXml} /> | ||||
<figcaption> | <figcaption> | ||||
@@ -16,7 +16,7 @@ const scoreXml = await loadScore(id); | |||||
</figure> | </figure> | ||||
</a> | </a> | ||||
<div class="screen-controls"> | <div class="screen-controls"> | ||||
<a href={`scores/${id}.musicxml`}> | |||||
<a href={`../scores/${id}.musicxml`}> | |||||
Download MusicXML | Download MusicXML | ||||
</a> | </a> | ||||
</div> | </div> | ||||
@@ -0,0 +1,7 @@ | |||||
--- | |||||
title: Piano Key Frequencies | |||||
--- | |||||
import { FrequenciesForm } from '../../components/FrequenciesForm'; | |||||
<FrequenciesForm client:load /> |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Limits | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Overcoming Doubts | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Fingering | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Pulse | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Sensation | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Autopilot | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: The Musician | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Ornaments | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Rests | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Gestures | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Articulation | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Expression | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Mistakes | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Performance | |||||
--- |
@@ -0,0 +1,3 @@ | |||||
--- | |||||
title: Thirds | |||||
--- |
@@ -0,0 +1,28 @@ | |||||
import { z, defineCollection } from 'astro:content'; | |||||
const specialCollection = defineCollection({ | |||||
type: 'content', | |||||
schema: z.object({ | |||||
title: z.string(), | |||||
}), | |||||
}); | |||||
const chaptersCollection = defineCollection({ | |||||
type: 'content', | |||||
schema: z.object({ | |||||
title: z.string(), | |||||
}), | |||||
}); | |||||
const appendicesCollection = defineCollection({ | |||||
type: 'content', | |||||
schema: z.object({ | |||||
title: z.string(), | |||||
}), | |||||
}); | |||||
export const collections = { | |||||
'special': specialCollection, | |||||
'chapters': chaptersCollection, | |||||
'appendices': appendicesCollection, | |||||
}; |
@@ -0,0 +1,186 @@ | |||||
--- | |||||
const { title, titlePrefix } = Astro.props.frontmatter || Astro.props; | |||||
--- | |||||
<!doctype html> | |||||
<html lang="en-PH"> | |||||
<head> | |||||
<meta charset="UTF-8" /> | |||||
<meta name="viewport" content="width=device-width" /> | |||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" /> | |||||
<title data-prefix={titlePrefix}>{title}</title> | |||||
<style is:global> | |||||
html { | |||||
@apply font-body; | |||||
} | |||||
body { | |||||
@apply w-full; | |||||
@apply mx-auto; | |||||
@apply max-w-screen-md; | |||||
@apply leading-relaxed; | |||||
@apply box-border; | |||||
} | |||||
p { | |||||
@apply my-6; | |||||
@apply indent-6; | |||||
@apply text-justify; | |||||
} | |||||
figure { | |||||
@apply m-0; | |||||
} | |||||
figcaption { | |||||
@apply text-center; | |||||
} | |||||
.screen-controls { | |||||
@apply flex; | |||||
@apply justify-center; | |||||
} | |||||
a:link { | |||||
@apply text-blue-600; | |||||
} | |||||
a:visited { | |||||
@apply text-purple-600; | |||||
} | |||||
div.score-wrapper { | |||||
@apply mt-8; | |||||
@apply mb-16; | |||||
} | |||||
title[data-prefix]::before { | |||||
@apply block; | |||||
@apply text-xl; | |||||
content: attr(data-prefix); | |||||
} | |||||
table { | |||||
@apply caption-bottom; | |||||
@apply w-full; | |||||
@apply border-spacing-0; | |||||
} | |||||
table caption { | |||||
@apply mt-4; | |||||
@apply px-16; | |||||
} | |||||
table.alternate-rows { | |||||
@apply border-0; | |||||
} | |||||
table.alternate-rows tbody tr:nth-child(2n + 1) * { | |||||
@apply relative; | |||||
} | |||||
table.alternate-rows tbody tr:nth-child(2n + 1) > *::before { | |||||
@apply absolute; | |||||
@apply top-0; | |||||
@apply left-0; | |||||
@apply w-full; | |||||
@apply h-full; | |||||
@apply bg-current; | |||||
@apply opacity-10; | |||||
@apply p-0; | |||||
content: ''; | |||||
} | |||||
@media only screen { | |||||
html { | |||||
@apply text-gray-800; | |||||
} | |||||
body { | |||||
@apply px-8; | |||||
@apply my-16; | |||||
} | |||||
div.score-wrapper figure svg { | |||||
@apply text-gray-800; | |||||
} | |||||
} | |||||
@media only print { | |||||
head { | |||||
@apply block; | |||||
@apply text-black; | |||||
} | |||||
div.score-wrapper figure svg { | |||||
@apply text-black; | |||||
} | |||||
title { | |||||
@apply block; | |||||
@apply text-3xl; | |||||
@apply text-center; | |||||
@apply mb-12; | |||||
@apply font-bold; | |||||
} | |||||
body { | |||||
@apply max-w-full; | |||||
@apply m-0; | |||||
} | |||||
.screen-controls { | |||||
@apply hidden; | |||||
} | |||||
a:link { | |||||
@apply text-inherit; | |||||
@apply no-underline; | |||||
} | |||||
a:visited { | |||||
@apply text-inherit; | |||||
} | |||||
figcaption { | |||||
@apply text-sm; | |||||
} | |||||
.print-hidden { | |||||
display: none; | |||||
} | |||||
} | |||||
@media only screen and (prefers-color-scheme: dark) { | |||||
html { | |||||
@apply bg-black; | |||||
@apply text-white; | |||||
} | |||||
div.score-wrapper figure svg { | |||||
@apply text-white; | |||||
} | |||||
img.score { | |||||
filter: invert(100%) grayscale(100%); | |||||
} | |||||
a:link { | |||||
@apply text-blue-400; | |||||
} | |||||
a:visited { | |||||
@apply text-purple-400; | |||||
} | |||||
} | |||||
@page { | |||||
margin: 2cm; | |||||
} | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<slot /> | |||||
</body> | |||||
</html> |
@@ -54,6 +54,10 @@ const { title } = Astro.props.frontmatter || Astro.props; | |||||
@apply mb-16; | @apply mb-16; | ||||
} | } | ||||
table { | |||||
caption-side: bottom; | |||||
} | |||||
@media only screen { | @media only screen { | ||||
html { | html { | ||||
@apply text-gray-800; | @apply text-gray-800; | ||||
@@ -108,6 +112,10 @@ const { title } = Astro.props.frontmatter || Astro.props; | |||||
figcaption { | figcaption { | ||||
@apply text-sm; | @apply text-sm; | ||||
} | } | ||||
.print-hidden { | |||||
display: none; | |||||
} | |||||
} | } | ||||
@media only screen and (prefers-color-scheme: dark) { | @media only screen and (prefers-color-scheme: dark) { |
@@ -0,0 +1,18 @@ | |||||
--- | |||||
import { getCollection } from 'astro:content'; | |||||
import Default from '../content/layouts/Default.astro'; | |||||
export const getStaticPaths = async () => { | |||||
const entries = await getCollection('special'); | |||||
return entries.map(entry => ({ | |||||
params: { slug: entry.slug }, props: { entry }, | |||||
})); | |||||
} | |||||
const { entry } = Astro.props; | |||||
const { Content } = await entry.render(); | |||||
--- | |||||
<Default title={entry.data.title}> | |||||
<Content /> | |||||
</Default> |
@@ -0,0 +1,19 @@ | |||||
--- | |||||
import { getCollection } from 'astro:content'; | |||||
import Layout from '../../content/layouts/Appendix.astro'; | |||||
export const getStaticPaths = async () => { | |||||
const entries = await getCollection('appendices'); | |||||
return entries.map(entry => ({ | |||||
params: { slug: entry.slug }, props: { entry }, | |||||
})); | |||||
} | |||||
const { entry } = Astro.props; | |||||
const { Content } = await entry.render(); | |||||
const titlePrefix = String.fromCharCode(parseInt(entry.slug) - 1 + 65); | |||||
--- | |||||
<Layout title={entry.data.title} titlePrefix={`Appendix ${titlePrefix}`}> | |||||
<Content /> | |||||
</Layout> |
@@ -1,8 +0,0 @@ | |||||
--- | |||||
title: Piano Key Frequencies | |||||
layout: ../layouts/Default.astro | |||||
--- | |||||
import { FrequenciesForm } from '../components/FrequenciesForm'; | |||||
<FrequenciesForm client:load /> |
@@ -0,0 +1,18 @@ | |||||
--- | |||||
import { getCollection } from 'astro:content'; | |||||
import Default from '../../content/layouts/Default.astro'; | |||||
export const getStaticPaths = async () => { | |||||
const entries = await getCollection('chapters'); | |||||
return entries.map(entry => ({ | |||||
params: { slug: entry.slug }, props: { entry }, | |||||
})); | |||||
} | |||||
const { entry } = Astro.props; | |||||
const { Content } = await entry.render(); | |||||
--- | |||||
<Default title={entry.data.title}> | |||||
<Content /> | |||||
</Default> |
@@ -0,0 +1,10 @@ | |||||
--- | |||||
import Default from '../content/layouts/Cover.astro'; | |||||
import { title } from '../../patchouli.book.json'; | |||||
--- | |||||
<Default title={title}> | |||||
<a href="toc.html"> | |||||
<img src="cover.jpg" alt={title} /> | |||||
</a> | |||||
</Default> |
@@ -1,7 +0,0 @@ | |||||
--- | |||||
title: Book Name | |||||
--- | |||||
<a href="toc.html"> | |||||
<img src="cover.jpg" alt={frontmatter.title} /> | |||||
</a> |
@@ -1,6 +1,6 @@ | |||||
--- | --- | ||||
import { readdir, readFile } from 'node:fs/promises'; | import { readdir, readFile } from 'node:fs/promises'; | ||||
const { default: Default } = await import('../layouts/Default.astro'); | |||||
import Default from '../content/layouts/Default.astro'; | |||||
type Frontmatter = Record<string, unknown>; | type Frontmatter = Record<string, unknown>; | ||||
@@ -9,7 +9,7 @@ interface PageProps { | |||||
} | } | ||||
const title = 'Table of Contents' | const title = 'Table of Contents' | ||||
const allPages = await readdir('src/pages'); | |||||
const allPages = await readdir('src/content/chapters'); | |||||
const pages = allPages.filter((p) => ( | const pages = allPages.filter((p) => ( | ||||
!p.startsWith('index.') | !p.startsWith('index.') | ||||
&& !p.endsWith('.astro') | && !p.endsWith('.astro') | ||||
@@ -18,7 +18,7 @@ const pages = allPages.filter((p) => ( | |||||
const pagesContentImported = await Promise.all( | const pagesContentImported = await Promise.all( | ||||
pages.map(async (p) => { | pages.map(async (p) => { | ||||
const fileBuffer = await readFile(`src/pages/${p}`); | |||||
const fileBuffer = await readFile(`src/content/chapters/${p}`); | |||||
const file = fileBuffer.toString('utf-8'); | const file = fileBuffer.toString('utf-8'); | ||||
const [, frontmatterRaw] = file.split('---'); | const [, frontmatterRaw] = file.split('---'); | ||||
@@ -39,7 +39,7 @@ const pagesContentImported = await Promise.all( | |||||
<ol> | <ol> | ||||
{pagesContentImported.map(([p, f]) => ( | {pagesContentImported.map(([p, f]) => ( | ||||
<li> | <li> | ||||
<a href={p.replace('.mdx', '')}> | |||||
<a href={`chapters/${p.replace('.mdx', '')}`}> | |||||
{f.frontmatter.title} | {f.frontmatter.title} | ||||
</a> | </a> | ||||
</li> | </li> | ||||