Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Thematic markdown files #44

Merged
merged 2 commits into from
Oct 16, 2021
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
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
pull_request:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2

# Runs a single command using the runners shell
- name: Install
run: yarn install

# Runs a set of commands using the runners shell
- name: Test
run: yarn test
6 changes: 6 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).ts?(x)'],
testPathIgnorePatterns: ['lib'],
};
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,33 @@
}
},
"scripts": {
"test": "echo \"Test is not implemented\"",
"build": "npm run tsup -- --minify",
"clean": "shx rm -rf lib",
"dev": "npm run tsup -- --watch",
"prepare": "npm run clean && npm run build",
"release": "release-it",
"release:dry": "release-it --dry-run",
"tsup": "tsup src/cli.ts -d lib"
"tsup": "tsup src/cli.ts -d lib",
"test": "jest",
"test:debug": "jest --silent=false --verbose false"
},
"dependencies": {
"chalk": "^4.1.2",
"create-create-app": "^7.1.0",
"execa": "^5.1.1"
"execa": "^5.1.1",
"upath": "^2.0.1"
},
"devDependencies": {
"@release-it/conventional-changelog": "^3.3.0",
"@types/jest": "^27.0.2",
"@types/node-fetch": "^3.0.2",
"husky": "^4.3.8",
"jest": "^27.2.5",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.1",
"release-it": "^14.11.6",
"shx": "^0.3.3",
"ts-jest": "^27.0.6",
"tsup": "^5.2.1",
"typescript": "^4.4.3"
}
Expand Down
24 changes: 20 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
#!/usr/bin/env node

import path from 'path'
import chalk from 'chalk'
import { AfterHookOptions, create } from 'create-create-app'
import { resolve } from 'path'
import { listThemes } from './themes'
import { thematicMarkdown } from './thematic-markdown'

const templateRoot = resolve(__dirname, '../templates')
/**
* The caveat message will be shown after the entire process is completed.
* @param options Options.
* @returns Message string.
*/
const caveat = (options: AfterHookOptions): string => {
try {
thematicMarkdown(options.packageDir)
} catch (err) {
// Since it is an extra process, no error is displayed.
}

const caveat = ({ name }: AfterHookOptions) => {
return `
${chalk.gray('1.')} cd ${chalk.bold.green(name)}
${chalk.gray('1.')} cd ${chalk.bold.green(options.name)}
${chalk.gray('2.')} create and edit Markdown files
${chalk.gray('3.')} edit ${chalk.bold.cyan(
'entry'
Expand All @@ -24,6 +34,9 @@ See ${chalk.yellow('https://docs.vivliostyle.org')} for further information.
`
}

/**
* Entry point.
*/
async function main() {
const themes = (await listThemes()).map((result) => ({
name: `${result.package.name} ${chalk.gray(
Expand All @@ -35,6 +48,8 @@ async function main() {
},
}))

const templateRoot = path.resolve(__dirname, '../templates')

await create('create-book', {
templateRoot,
extra: {
Expand All @@ -51,4 +66,5 @@ async function main() {
caveat,
})
}

main()
137 changes: 137 additions & 0 deletions src/thematic-markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import upath from 'upath'
import fs from 'fs'

/** Config filename of Vivliostyle. */
const CONFIG_FILE_NAME = 'vivliostyle.config.js'

/** Default markdown filename of Create Book. */
const DEFAULT_MARKDOWN_FILE_NAME = 'manuscript.md'

/** Name to rename the default file when copying Markdown files. */
const ESCAPED_MARKDOWN_FILE_NAME = '_manuscript.md'

/** Separator of `entry` element in` vivliostyle.config.js`. */
const ENTRIES_SEPARATOR = ',\n '

/**
* Get the path to the directory of the theme installed in the project generated by Create Book.
* @param projectDir The path to the project directory generated by Create Book.
* @returns A path string on success, an empty string on failure.
* @throws Failed to load `vivliostyle.config.js`.
*/
const getThemeDir = (projectDir: string): string => {
const config = require(upath.join(projectDir, CONFIG_FILE_NAME))
if (!(config && typeof config.theme === 'string')) {
return ''
}

return upath.normalize(upath.join(projectDir, 'node_modules', config.theme))
}

/**
* Rename the default Markdown so it doesn't conflict with the theme's.
* @param projectDir The path to the project directory generated by Create Book.
*/
const escapeDefaultMarkdownFile = (projectDir: string) => {
const src = upath.join(projectDir, DEFAULT_MARKDOWN_FILE_NAME)
const dest = upath.join(projectDir, ESCAPED_MARKDOWN_FILE_NAME)
fs.renameSync(src, dest)
}

/**
* Restore the Markdown file in your project's directory to its original state.
* @param projectDir The path to the project directory generated by Create Book.
* @param copiedFileNames Markdown file name list copied from theme to project directory.
*/
const restoreDefaultMarkdownFile = (
projectDir: string,
copiedFileNames: string[]
) => {
copiedFileNames.forEach((fileName) => {
const filePath = upath.join(projectDir, fileName)
fs.unlinkSync(filePath)
})

const src = upath.join(projectDir, ESCAPED_MARKDOWN_FILE_NAME)
const dest = upath.join(projectDir, DEFAULT_MARKDOWN_FILE_NAME)
fs.renameSync(src, dest)
}

/**
* Copy the Markdown files.
* @param projectDir The path to the project directory generated by Create Book.
* @param entries Relative path of the Markdown files on theme directory, e.g. '["example/foo.md", "bar.md"]'
* @returns Name of the copied files.
*/
const copyMarkdownFiles = (
themeDir: string,
entries: string[],
projectDir: string
): string[] => {
const results: string[] = []
if (entries.length === 0) {
return results
}

escapeDefaultMarkdownFile(projectDir)

for (const entry of entries) {
const src = upath.normalize(upath.join(themeDir, entry))
const newEntry = upath.basename(src)
const dest = upath.normalize(upath.join(projectDir, newEntry))
try {
fs.copyFileSync(src, dest)
results.push(newEntry)
} catch (err) {
// If even one fails, restore it to the default state so as not to make it incomplete.
restoreDefaultMarkdownFile(projectDir, results)
return []
}
}

fs.unlinkSync(upath.join(projectDir, ESCAPED_MARKDOWN_FILE_NAME))

return results
}

/**
* Replace `entry` in your project's config file with a copied Markdown file.
* @param projectDir The path to the project directory generated by Create Book.
* @param entries Name of the Markdown files. e.g. '["foo.md", "bar.md"]'
*/
const replaceConfigEntries = (projectDir: string, entries: string[]) => {
const filePath = upath.join(projectDir, CONFIG_FILE_NAME)
const text = fs.readFileSync(filePath, 'utf-8')
const entriesText =
entries.map((entry) => `'${entry}'`).join(ENTRIES_SEPARATOR) + ','
const newText = text.replace(`'manuscript.md',`, entriesText)
fs.writeFileSync(filePath, newText)
}

/**
* Apply thematic Markdown.
* The markdown file is based on the `vivliostyle.config.js` definition of the installed theme.
* @param projectDir The path to the project directory generated by Create Book.
* @returns `true` on success, `false` on failure.
* @throws Failed to load `vivliostyle.config.js`.
*/
export const thematicMarkdown = (projectDir: string): boolean => {
const themeDir = getThemeDir(projectDir)
if (themeDir === '') {
return false
}

const config = require(upath.join(themeDir, CONFIG_FILE_NAME))
if (!(config && Array.isArray(config.entry))) {
return false
}

const newEntries = copyMarkdownFiles(themeDir, config.entry, projectDir)
if (newEntries.length === 0) {
return false
}

replaceConfigEntries(projectDir, newEntries)

return true
}
1 change: 1 addition & 0 deletions tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!node_modules/

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions tests/sample/vivliostyle.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module.exports = {
title: 'sample', // populated into `publication.json`, default to `title` of the first entry or `name` in `package.json`.
author: 'akabeko <[email protected]>', // default to `author` in `package.json` or undefined.
// language: 'ja', // default to undefined.
// size: 'A4', // paper size.
theme: '@vivliostyle/theme-slide', // .css or local dir or npm package. default to undefined.
entry: [
'manuscript.md', // `title` is automatically guessed from the file (frontmatter > first heading).
// {
// path: 'epigraph.md',
// title: 'Epigraph', // title can be overwritten (entry > file),
// theme: '@vivliostyle/theme-whatever', // theme can be set individually. default to the root `theme`.
// },
// 'glossary.html', // html can be passed.
], // `entry` can be `string` or `object` if there's only single markdown file.
// entryContext: './manuscripts', // default to '.' (relative to `vivliostyle.config.js`).
// output: [ // path to generate draft file(s). default to '{title}.pdf'
// './output.pdf', // the output format will be inferred from the name.
// {
// path: './book',
// format: 'webpub',
// },
// ],
// workspaceDir: '.vivliostyle', // directory which is saved intermediate files.
// toc: true, // whether generate and include ToC HTML or not, default to 'false'.
// cover: './cover.png', // cover image. default to undefined.
// vfm: { // options of VFM processor
// hardLineBreaks: true, // converts line breaks of VFM to <br> tags. default to 'false'.
// disableFormatHtml: true, // disables HTML formatting. default to 'false'.
// },
}
49 changes: 49 additions & 0 deletions tests/thematic-markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import fs from 'fs'
import path from 'path'
import { thematicMarkdown } from '../src/thematic-markdown'

/**
* It is processed safely by checking the existence and then deleting the file.
* @param filePath Path of the target file.
*/
const deleteFileSync = (filePath: string) => {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
}

describe('Thematic Markdown', () => {
const projectDir = path.resolve('./tests/sample')
const defaultFilePath = path.join(projectDir, 'manuscript.md')
const fooFilePath = path.join(projectDir, 'foo.md')
const barFilePath = path.join(projectDir, 'bar.md')
const configFilePath = path.join(projectDir, 'vivliostyle.config.js')
const escapedConfigFilePath = path.join(projectDir, '_vivliostyle.config.js')

beforeEach(() => {
fs.writeFileSync(defaultFilePath, 'Sample', 'utf-8')
fs.copyFileSync(configFilePath, escapedConfigFilePath)
})

afterEach(() => {
deleteFileSync(fooFilePath)
deleteFileSync(barFilePath)
deleteFileSync(defaultFilePath)
deleteFileSync(configFilePath)
fs.renameSync(escapedConfigFilePath, configFilePath)
})

it('Replace', () => {
thematicMarkdown(projectDir)
expect(fs.existsSync(fooFilePath)).toBeTruthy()
expect(fs.existsSync(barFilePath)).toBeTruthy()

// Evaluation by require has a cache problem, so check the string
// `delete require.cache[file]` didn't work
const config = fs.readFileSync(configFilePath, 'utf-8')
const entry = `entry: [
'foo.md',
'bar.md', // `
expect(config.indexOf(entry) !== -1).toBeTruthy()
})
})
Loading