import path from 'node:path' import process from 'node:process' import fs from 'fs-extra' import type { SupportedLocale } from './locales' import { defaultLocale, f, supportedLocales, t } from './locales' import { loadQuizzes, resolveInfo } from './loader' import { toAnswerShort, toNearborREADME, toPlayShort, toQuizREADME, toSolutionsShort } from './toUrl' import type { Quiz, QuizMetaInfo } from './types' const DifficultyColors: Record = { warm: 'teal', easy: '7aad0c', medium: 'd9901a', hard: 'de3d37', extreme: 'b11b8d', } const DifficultyRank = [ 'warm', 'easy', 'medium', 'hard', 'extreme', ] function escapeHtml(unsafe: string) { return unsafe .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } function toBadgeURL(label: string, text: string, color: string, args = '') { return `https://img.shields.io/badge/${encodeURIComponent(label.replace(/-/g, '--'))}-${encodeURIComponent(text.replace(/-/g, '--'))}-${color}${args}` } function toBadge(label: string, text: string, color: string, args = '') { return `${text}` } export function toBadgeLink(url: string, label: string, text: string, color: string, args = '') { return `${toBadge(label, text, color, args)} ` } export function toPlanTextLink(url: string, _label: string, text: string, _color: string, _args = '') { return `${text} ` } function toAuthorInfo(author: Partial = {}) { return `by ${author.name}${author.github ? ` @${author.github}` : ''}` } function toDifficultyBadge(difficulty: string, locale: SupportedLocale) { return toBadge('', t(locale, `difficulty.${difficulty}`), DifficultyColors[difficulty]) } function toDifficultyBadgeInverted(difficulty: string, locale: SupportedLocale, count: number) { return toBadge(t(locale, `difficulty.${difficulty}`), count.toString(), DifficultyColors[difficulty]) } function toDifficultyPlainText(difficulty: string, locale: SupportedLocale, count: number) { return `${t(locale, `difficulty.${difficulty}`)} (${count.toString()})` } function toDetailsInnerText(text: string, locale: SupportedLocale) { return `${t(locale, `details.${text}`)}` } function quizToBadge(quiz: Quiz, locale: string, absolute = false, badge = true) { const fn = badge ? toBadgeLink : toPlanTextLink return fn( toQuizREADME(quiz, locale, absolute), '', `${quiz.no}・${quiz.info[locale]?.title || quiz.info[defaultLocale]?.title}`, DifficultyColors[quiz.difficulty], ) } function quizNoToBadges(ids: (string | number)[], quizzes: Quiz[], locale: string, absolute = false) { return ids .map(i => quizzes.find(q => q.no === Number(i))) .filter(Boolean) .map(i => quizToBadge(i!, locale, absolute)) .join(' ') } function getAllTags(quizzes: Quiz[], locale: string) { const set = new Set() for (const quiz of quizzes) { const info = resolveInfo(quiz, locale) for (const tag of (info?.tags || [])) set.add(tag) } return Array.from(set).sort() } function getQuizzesByTag(quizzes: Quiz[], locale: string, tag: string) { return quizzes.filter((quiz) => { const info = resolveInfo(quiz, locale) return !!info.tags?.includes(tag) }) } async function insertInfoReadme(filepath: string, quiz: Quiz, locale: SupportedLocale, quizzes: Quiz[]) { if (!fs.existsSync(filepath)) return let text = await fs.readFile(filepath, 'utf-8') /* eslint-disable prefer-template */ if (!text.match(/[\s\S]*/)) text = `\n\n${text}` if (!text.match(/[\s\S]*/)) text = `${text}\n\n` const info = resolveInfo(quiz, locale) const availableLocales = supportedLocales.filter(l => l !== locale).filter(l => !!quiz.readme[l]) text = text .replace( /[\s\S]*/, '' + `

${escapeHtml(info.title || '')} ${toDifficultyBadge(quiz.difficulty, locale)} ${(info.tags || []).map(i => toBadge('', `#${i}`, '999')).join(' ')}

` + `

${toAuthorInfo(info.author)}

` + '

' + toBadgeLink(toPlayShort(quiz.no, locale), '', t(locale, 'badge.take-the-challenge'), '3178c6', '?logo=typescript&logoColor=white') + (availableLocales.length ? ('   ' + availableLocales.map(l => toBadgeLink(toNearborREADME(quiz, l), '', t(l, 'display'), 'gray')).join(' ')) : '') + '

' + '', ) .replace( /[\s\S]*/, '
' + toBadgeLink(`../../${f('README', locale, 'md')}`, '', t(locale, 'badge.back'), 'grey') + toBadgeLink(toAnswerShort(quiz.no, locale), '', t(locale, 'badge.share-your-solutions'), 'teal') + toBadgeLink(toSolutionsShort(quiz.no), '', t(locale, 'badge.checkout-solutions'), 'de5a77', '?logo=awesome-lists&logoColor=white') + (Array.isArray(info.related) && info.related.length ? `

${t(locale, 'readme.related-challenges')}

${quizNoToBadges(info.related, quizzes, locale, true)}` : '') + '', ) /* eslint-enable prefer-template */ await fs.writeFile(filepath, text, 'utf-8') } async function updateIndexREADME(quizzes: Quiz[]) { // update index README for (const locale of supportedLocales) { const filepath = path.resolve(__dirname, '..', f('README', locale, 'md')) let challengesREADME = '' let prev = '' // difficulty const quizzesByDifficulty = [...quizzes].sort((a, b) => DifficultyRank.indexOf(a.difficulty) - DifficultyRank.indexOf(b.difficulty)) for (const quiz of quizzesByDifficulty) { if (prev !== quiz.difficulty) challengesREADME += `${prev ? '

' : ''}${toDifficultyBadgeInverted(quiz.difficulty, locale, quizzesByDifficulty.filter(q => q.difficulty === quiz.difficulty).length)}
` challengesREADME += quizToBadge(quiz, locale) prev = quiz.difficulty } // by tags challengesREADME += `
${toDetailsInnerText('by-tags', locale)}
` const tags = getAllTags(quizzes, locale) for (const tag of tags) { challengesREADME += `' } challengesREADME += '' challengesREADME += '
${toBadge('', `#${tag}`, '999')}` getQuizzesByTag(quizzesByDifficulty, locale, tag) .forEach((quiz) => { challengesREADME += quizToBadge(quiz, locale) }) challengesREADME += '
          
' // by plain text prev = '' challengesREADME += `
${toDetailsInnerText('by-plain-text', locale)}
` for (const quiz of quizzesByDifficulty) { if (prev !== quiz.difficulty) challengesREADME += `${prev ? '' : ''}

${toDifficultyPlainText(quiz.difficulty, locale, quizzesByDifficulty.filter(q => q.difficulty === quiz.difficulty).length)}

    ` challengesREADME += `
  • ${quizToBadge(quiz, locale, false, false)}
  • ` prev = quiz.difficulty } challengesREADME += '

' let readme = await fs.readFile(filepath, 'utf-8') readme = readme.replace( /[\s\S]*/m, `\n${challengesREADME}\n`, ) await fs.writeFile(filepath, readme, 'utf-8') } } async function updateQuestionsREADME(quizzes: Quiz[]) { const questionsDir = path.resolve(__dirname, '../questions') // update each questions' readme for (const quiz of quizzes) { for (const locale of supportedLocales) { await insertInfoReadme( path.join( questionsDir, quiz.path, f('README', locale, 'md'), ), quiz, locale, quizzes, ) } } } export async function updateREADMEs(type?: 'quiz' | 'index') { const quizzes = await loadQuizzes() quizzes.sort((a, b) => a.no - b.no) if (type === 'quiz') { await updateQuestionsREADME(quizzes) } else if (type === 'index') { await updateIndexREADME(quizzes) } else { await Promise.all([ updateIndexREADME(quizzes), updateQuestionsREADME(quizzes), ]) } } updateREADMEs(process.argv.slice(2)[0] as any)