<|start_filename|>scripts/gfm/fetch.js<|end_filename|>
/**
 * 将 GFM 中的 example 元素解析成 JSON 数据
 * @param exampleEl example 元素
 */
function parseGFMExample(exampleEl) {
  if (exampleEl == null) return null

  const exampleId = exampleEl.id || ''
  const exampleNo = exampleId.replace(/^example-(\d+)$/, '$1')
  if (exampleNo.length < 0) {
    console.error(`Bad exampleEl: id(${exampleEl.id}). skipped`)
    return null
  }

  const getCode = column => {
    const codeEl = column.getElementsByTagName('code')[0]
    const content = codeEl.innerText.replace(/\n$/, '')
    return content
  }

  const getDescription = el => {
    const prevEl = el.previousElementSibling
    if (prevEl == null) return undefined
    if (prevEl.tagName.toLowerCase() === 'p') {
      return prevEl.innerText.replace(/:$/, '')
    }
    if (prevEl.classList.contains('example')) {
      return getDescription(prevEl)
    }
  }

  const { origin, pathname } = window.location
  const columns = exampleEl.getElementsByClassName('column')
  const result = {
    title: `GFM#${exampleNo} ${origin}${pathname}#${exampleId}`,
    description: getDescription(exampleEl),
    content: getCode(columns[0]).replace(/→/g, '\t'),
    expectedHtml: getCode(columns[1]).replace(/→/g, '\t'),
  }
  return result
}

function fetchGFMExamples(...exampleNos) {
  const exampleEls = exampleNos.map(exampleNo => {
    const id = 'example-' + exampleNo
    const elementEl = document.getElementById(id)
    return elementEl
  })
  let result = exampleEls.map(exampleEl => parseGFMExample(exampleEl))
  if (result.length === 1) result = result[0]
  return JSON.stringify(result, null, 2)
}

function fetchGFMExamplesInRange(left, right = left) {
  const exampleNos = []
  for (let i = left; i <= right; ++i) exampleNos.push(i)
  return fetchGFMExamples(...exampleNos)
}

function fetchExamplesAll() {
  const exampleEls = document.querySelectorAll('[id^=example]')
  const result = Array.from(exampleEls).map(exampleEl => parseGFMExample(exampleEl))
  return JSON.stringify(result, null, 2)
}

module.exports = {
  fetchExamplesAll,
  fetchGFMExamples,
  fetchGFMExamplesInRange,
  parseGFMExample,
}


<|start_filename|>jest.config.js<|end_filename|>
const { tsMonorepoConfig } = require('@guanghechen/jest-config')

module.exports = {
  ...tsMonorepoConfig(__dirname),
  testRegex: '/(__test__)/[\\s\\S]+\\.spec\\.[jt]sx?$',
  coveragePathIgnorePatterns: [
    /packages[/\\]character[/\\]src[/\\]util[/\\]node-point\.ts/.source,
    /packages[/\\]parser[/\\]src[/\\]types\.ts/.source,
    /packages[/\\]parser-gfm[/\\]src[/\\]types\.ts/.source,
    /tokenizers[/\\][^/]*[/\\]src[/\\]index\.ts/.source,
  ],
  coverageThreshold: {
    global: {
      branches: 50,
      functions: 80,
      lines: 60,
      statements: 60,
    },
  },
}


<|start_filename|>scaffolds/eslint-config/index.js<|end_filename|>
module.exports = {
  root: true,
  extends: ['@guanghechen', 'prettier'],
  plugins: ['import'],
  env: {
    browser: true,
    commonjs: true,
    es6: true,
    jest: true,
    node: true,
  },
  overrides: [
    {
      files: ['**/*.ts'],
      extends: ['@guanghechen', '@guanghechen/ts', 'prettier'],
    },
    {
      files: ['**/__test__/*.spec.ts'],
      extends: ['@guanghechen', '@guanghechen/ts', 'plugin:jest/recommended', 'prettier'],
      rules: {
        '@typescript-eslint/consistent-type-assertions': 0,
        'import/no-extraneous-dependencies': 0,
        'jest/expect-expect': 0,
      },
    },
    {
      files: ['**/__test__/*.spec.js'],
      extends: ['@guanghechen', 'plugin:jest/recommended', 'prettier'],
      rules: {
        'import/no-extraneous-dependencies': 0,
        'jest/expect-expect': 0,
      },
    },
  ],
}


<|start_filename|>.vscode/settings.json<|end_filename|>
{
  "cSpell.words": [
    "Autolinks",
    "Builtins",
    "Frontmatter",
    "Interop",
    "Parens",
    "Postorder",
    "Preorder",
    "Strikethrough",
    "Yast",
    "backticks",
    "barusu",
    "blockquote",
    "blockquotes",
    "boilerplates",
    "cond",
    "deduplication",
    "dirpath",
    "displaystyle",
    "docpage",
    "downlevel",
    "editorconfig",
    "eslintconfig",
    "eslintignore",
    "gitmoji",
    "globby",
    "gmail",
    "guanghechen",
    "inlines",
    "interruptable",
    "kase",
    "lcov",
    "lifecycle",
    "mathjax",
    "mdast",
    "middlewares",
    "multipriority",
    "pinst",
    "plopfile",
    "plusplus",
    "postbuild",
    "postpublish",
    "prebuild",
    "prepending",
    "prettierrc",
    "readonly",
    "recognised",
    "rimraf",
    "rollup",
    "setext",
    "softbreak",
    "sourcecode",
    "sourcemap",
    "standardly",
    "styl",
    "tsbuildinfo",
    "typeof",
    "unmount",
    "whitespaces",
    "yozora"
  ],
  "files.associations": {
    ".prettierrc": "yaml"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "typescript.tsdk": "node_modules/typescript/lib"
}


<|start_filename|>scripts/unicode/fetch.js<|end_filename|>
/**
 * @see https://www.fileformat.info/info/unicode/category/Pd/list.htm
 */
function fetchUnicodeCharacter(tbody) {
  const trList = tbody.querySelectorAll('tr')
  const items = []
  for (let i = 0; i < trList.length; ++i) {
    const tr = trList[i]
    const tdList = tr.querySelectorAll('td')
    const valueText = tdList[0].innerText
    const nameText = tdList[1].innerText
    const url = tdList[0].querySelector('a').href
    if (valueText == null) continue

    const name = nameText.trim().split(/[\s-]/g).join('_')
    const value = valueText.trim().toLowerCase().replace(/^u\+/i, '0x0')
    const item = '  /**\n   ' + '* @see ' + url + '\n   */\n  ' + `${name} = ${value},`
    items.push(item)
  }
  return items.join('\n')
}

module.exports = {
  fetchUnicodeCharacter,
}


<|start_filename|>tsconfig.json<|end_filename|>
{
  "extends": "./tsconfig.settings.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@yozora/ast": ["./packages/ast/src"],
      "@yozora/ast-util": ["./packages/ast-util/src"],
      "@yozora/character": ["./packages/character/src"],
      "@yozora/core-parser": ["./packages/core-parser/src"],
      "@yozora/core-tokenizer": ["./packages/core-tokenizer/src"],
      "@yozora/parser": ["./packages/parser/src"],
      "@yozora/parser-gfm": ["./packages/parser-gfm/src"],
      "@yozora/parser-gfm-ex": ["./packages/parser-gfm-ex/src"],
      "@yozora/jest-for-tokenizer": ["./scaffolds/jest-for-tokenizer/src"],
      "@yozora/tokenizer-admonition": ["./tokenizers/admonition/src"],
      "@yozora/tokenizer-autolink": ["./tokenizers/autolink/src"],
      "@yozora/tokenizer-autolink-extension": ["./tokenizers/autolink-extension/src"],
      "@yozora/tokenizer-blockquote": ["./tokenizers/blockquote/src"],
      "@yozora/tokenizer-break": ["./tokenizers/break/src"],
      "@yozora/tokenizer-definition": ["./tokenizers/definition/src"],
      "@yozora/tokenizer-delete": ["./tokenizers/delete/src"],
      "@yozora/tokenizer-emphasis": ["./tokenizers/emphasis/src"],
      "@yozora/tokenizer-ecma-import": ["./tokenizers/ecma-import/src"],
      "@yozora/tokenizer-fenced-block": ["./tokenizers/fenced-block/src"],
      "@yozora/tokenizer-fenced-code": ["./tokenizers/fenced-code/src"],
      "@yozora/tokenizer-footnote": ["./tokenizers/footnote/src"],
      "@yozora/tokenizer-footnote-definition": ["./tokenizers/footnote-definition/src"],
      "@yozora/tokenizer-footnote-reference": ["./tokenizers/footnote-reference/src"],
      "@yozora/tokenizer-heading": ["./tokenizers/heading/src"],
      "@yozora/tokenizer-html-block": ["./tokenizers/html-block/src"],
      "@yozora/tokenizer-html-inline": ["./tokenizers/html-inline/src"],
      "@yozora/tokenizer-image": ["./tokenizers/image/src"],
      "@yozora/tokenizer-image-reference": ["./tokenizers/image-reference/src"],
      "@yozora/tokenizer-indented-code": ["./tokenizers/indented-code/src"],
      "@yozora/tokenizer-inline-code": ["./tokenizers/inline-code/src"],
      "@yozora/tokenizer-inline-math": ["./tokenizers/inline-math/src"],
      "@yozora/tokenizer-invariant": ["./tokenizers/invariant/src"],
      "@yozora/tokenizer-link": ["./tokenizers/link/src"],
      "@yozora/tokenizer-link-reference": ["./tokenizers/link-reference/src"],
      "@yozora/tokenizer-list": ["./tokenizers/list/src"],
      "@yozora/tokenizer-math": ["./tokenizers/math/src"],
      "@yozora/tokenizer-paragraph": ["./tokenizers/paragraph/src"],
      "@yozora/tokenizer-setext-heading": ["./tokenizers/setext-heading/src"],
      "@yozora/tokenizer-table": ["./tokenizers/table/src"],
      "@yozora/tokenizer-text": ["./tokenizers/text/src"],
      "@yozora/tokenizer-thematic-break": ["./tokenizers/thematic-break/src"],
      "jest.setup": ["jest.setup.ts"]
    }
  }
}


<|start_filename|>scripts/gfm/classify.js<|end_filename|>
/* eslint-disable import/no-extraneous-dependencies */
const chalk = require('chalk')
const fs = require('fs-extra')
const path = require('path')
const gfmClassifyData = require('./data.json')

class GFMExampleClassifier {
  constructor(gfmExamples) {
    this.gfmExamples = gfmExamples
    this.numberLength = gfmExamples.length.toString().length
  }

  classifyToPath(caseRootDir, groups) {
    const self = this
    for (const group of groups) {
      const excluded = group.excluded || []
      const groupDir = path.resolve(caseRootDir, group.name)
      if (!fs.existsSync(groupDir)) fs.mkdirpSync(groupDir)
      for (let i = group.start; i <= group.end; ++i) {
        if (excluded.includes(i)) continue
        const fileName = '#' + ('' + i).padStart(self.numberLength, '0') + '.json'
        const caseFilePath = path.join(groupDir, fileName)
        const gfmExample = self.gfmExamples[i]
        const data = self.mapGFMExampleDataToCase(gfmExample)
        const content = JSON.stringify(data, null, 2)
        fs.writeFileSync(caseFilePath, content, 'utf-8')
        console.log(chalk.green(`Add case ${caseFilePath}`))
      }
    }
    return self
  }

  mapGFMExampleDataToCase(gfmExample) {
    const result = {
      title: gfmExample.title,
      cases: [
        {
          description: gfmExample.description,
          input: gfmExample.content,
          htmlAnswer: gfmExample.expectedHtml,
        },
      ],
    }
    return result
  }
}

const classifier = new GFMExampleClassifier(gfmClassifyData)

const rootDir = path.resolve(__dirname, '../../')

// parser-gfm test cases
classifier.classifyToPath(
  path.resolve(rootDir, 'scaffolds/jest-for-tokenizer/fixtures/gfm'),
  require('./data-classify/parser-gfm.json'),
)


<|start_filename|>scaffolds/template-tokenizer/index.js<|end_filename|>
const { toKebabCase, toTrim, convertToBoolean } = require('@guanghechen/option-helper')
const {
  createNpmPackagePrompts,
  resolveNpmPackageAnswers,
  resolveNpmPackagePreAnswers,
} = require('@guanghechen/plop-helper')
const path = require('path')
const manifest = require('./package.json')

const transformers = {
  tokenizerName: toTrim,
}

module.exports = function (plop) {
  const preAnswers = resolveNpmPackagePreAnswers({
    isMonorepo: convertToBoolean(process.env.DEBUG_IS_MONOREPO),
  })
  const defaultAnswers = { packageVersion: manifest.version }
  const { cwd, isMonorepo } = preAnswers
  const tokenizerPackageNameRegex = /^(?:[^\\/]+\/)tokenizer-([\w-]+)$/

  const prompts = createNpmPackagePrompts(preAnswers, defaultAnswers)
  prompts.splice(
    1,
    0,
    {
      type: 'input',
      name: 'tokenizerName',
      message: 'tokenizer name',
      default: ({ packageName }) => {
        let m = tokenizerPackageNameRegex.exec(packageName)
        if (m == null) return packageName.replace(/^[^\\/]+[\\/]/, '')
        return m[1]
      },
      transformer: transformers.tokenizerName,
      validate: text => /^[\w-]+|@[\w-]+\/[\w-]+$/.test(text),
    },
    {
      type: 'list',
      name: 'tokenizerCategory',
      message: 'tokenizer category',
      choices: ['block', 'inline'],
      default: answers => {
        if (/block/.test(answers.packageName)) return 'block'
        if (/inline/.test(answers.packageName)) return 'inline'
        return 'block'
      },
    },
  )

  plop.setGenerator('tokenizer', {
    description: 'create tokenizer template project',
    prompts: [...prompts],
    actions: function (_answers) {
      const answers = resolveNpmPackageAnswers(preAnswers, _answers)
      answers.tokenizerName = transformers.tokenizerName(_answers.tokenizerName)
      answers.tokenizerCategory = _answers.tokenizerCategory

      switch (answers.tokenizerCategory) {
        case 'block':
          answers.isBlockTokenizer = true
          answers.fallbackTokenizerName = 'paragraph'
          break
        case 'inline':
          answers.isInlineTokenizer = true
          answers.fallbackTokenizerName = 'text'
          break
      }

      const resolveSourcePath = p => path.normalize(path.resolve(__dirname, 'boilerplate', p))
      const resolveTargetPath = p => path.normalize(path.resolve(answers.packageLocation, p))
      const relativePath = path.relative(answers.packageLocation, cwd)

      const { tokenizerName, tokenizerCategory } = answers
      answers.tsconfigExtends = answers.isMonorepo
        ? path.join(relativePath, 'tsconfig')
        : './tsconfig.settings'
      answers.tsconfigSrcExtends = answers.isMonorepo
        ? path.join(relativePath, 'tsconfig.settings')
        : './tsconfig.settings'
      answers.nodeModulesPath = path.join(relativePath, 'node_modules')
      answers.toolPackageVersion = manifest.version

      // Assign resolved data into plop templates.
      Object.assign(_answers, answers)

      if (_answers.isDebugMode) {
        console.debug('answers:', answers)
        const debugOptions = _answers.debugOptions || {}
        if (debugOptions.shouldGenerateFiles === false) return []
      }

      return [
        !isMonorepo && {
          type: 'add',
          path: resolveTargetPath('.eslintrc'),
          templateFile: resolveSourcePath('.eslintrc.hbs'),
        },
        !isMonorepo && {
          type: 'add',
          path: resolveTargetPath('rollup.config.js'),
          templateFile: resolveSourcePath('rollup.config.js.hbs'),
        },
        !isMonorepo && {
          type: 'add',
          path: resolveTargetPath('.editorconfig'),
          templateFile: resolveSourcePath('.editorconfig.hbs'),
        },
        !isMonorepo && {
          type: 'add',
          path: resolveTargetPath('.gitignore'),
          templateFile: resolveSourcePath('.gitignore.hbs'),
        },
        !isMonorepo && {
          type: 'add',
          path: resolveTargetPath('.prettierrc'),
          templateFile: resolveSourcePath('.prettierrc.hbs'),
        },
        !isMonorepo && {
          type: 'add',
          path: resolveTargetPath('jest.config.js'),
          templateFile: resolveSourcePath('jest.config.js.hbs'),
        },
        !isMonorepo && {
          type: 'add',
          path: resolveTargetPath('tsconfig.settings.json'),
          templateFile: resolveSourcePath('tsconfig.settings.json.hbs'),
        },
        {
          type: 'add',
          path: resolveTargetPath('package.json'),
          templateFile: resolveSourcePath('package.json.hbs'),
        },
        {
          type: 'add',
          path: resolveTargetPath('README.md'),
          templateFile: resolveSourcePath('README.md.hbs'),
        },
        {
          type: 'add',
          path: resolveTargetPath('tsconfig.json'),
          templateFile: resolveSourcePath('tsconfig.json.hbs'),
        },
        {
          type: 'add',
          path: resolveTargetPath('tsconfig.src.json'),
          templateFile: resolveSourcePath('tsconfig.src.json.hbs'),
        },
        {
          type: 'add',
          path: resolveTargetPath('src/index.ts'),
          templateFile: resolveSourcePath(`${tokenizerCategory}-tokenizer/src/index.ts.hbs`),
        },
        {
          type: 'add',
          path: resolveTargetPath('src/match.ts'),
          templateFile: resolveSourcePath(`${tokenizerCategory}-tokenizer/src/match.ts.hbs`),
        },
        {
          type: 'add',
          path: resolveTargetPath('src/parse.ts'),
          templateFile: resolveSourcePath(`${tokenizerCategory}-tokenizer/src/parse.ts.hbs`),
        },
        {
          type: 'add',
          path: resolveTargetPath('src/tokenizer.ts'),
          templateFile: resolveSourcePath(`${tokenizerCategory}-tokenizer/src/tokenizer.ts.hbs`),
        },
        {
          type: 'add',
          path: resolveTargetPath('src/types.ts'),
          templateFile: resolveSourcePath(`${tokenizerCategory}-tokenizer/src/types.ts.hbs`),
        },
        {
          type: 'add',
          path: resolveTargetPath('__test__/answer.ts'),
          templateFile: resolveSourcePath(`${tokenizerCategory}-tokenizer/__test__/answer.ts.hbs`),
        },
        {
          type: 'add',
          path: resolveTargetPath(`__test__/${toKebabCase(tokenizerName)}.spec.ts`),
          templateFile: resolveSourcePath(
            `${tokenizerCategory}-tokenizer/__test__/suite.spec.ts.hbs`,
          ),
        },
        {
          type: 'add',
          path: resolveTargetPath('__test__/fixtures/basic.json'),
          templateFile: resolveSourcePath(
            `${tokenizerCategory}-tokenizer/__test__/fixtures/basic.json.hbs`,
          ),
        },
      ].filter(Boolean)
    },
  })
}
