-
Notifications
You must be signed in to change notification settings - Fork 82
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Auto generate sidebar and index. (#30)
* add script * add spacing to summary so it works. * Add games.json for ウィスキー Bot * Fix games with no aliases in games.json gen * Fix file paths containg .md.html in generated games.json * Regen using script after new games were added * Fix dates and add ratings to games.json * Add new games and fix one more date. * Update script to not update README.md and to use git to get last updated * Refactor getLastUpdated function to handle invalid dates * Refactor system to do linting. * Whoops, forgot to commit (I hope this gets squashed) * Test invalid lint via amongus + add trailing new line * Test if github anotations work * Fix lint (: * Update game support status descriptions * Remove "a"
- Loading branch information
1 parent
9730a61
commit 6ef7c27
Showing
102 changed files
with
1,496 additions
and
185 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
on: | ||
push: | ||
pull_request: | ||
workflow_dispatch: | ||
|
||
jobs: | ||
lint: | ||
runs-on: ubuntu-latest | ||
env: | ||
GITHUB_ACTIONS: true | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: actions/setup-node@v4 | ||
with: | ||
node-version: latest | ||
- run: node scripts/lint.mjs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
book | ||
.idea | ||
**/.DS_Store | ||
src/games.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
singleQuote: true | ||
semi: true | ||
trailingComma: none | ||
arrowParens: avoid | ||
endOfLine: lf | ||
tabWidth: 4 | ||
printWidth: 80 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
/** | ||
* @fileoverview Core shared functions between linting and generation. | ||
*/ | ||
import { readdir } from 'node:fs/promises'; | ||
import { getDirName, logging } from './utils.mjs'; | ||
import { resolve } from 'node:path'; | ||
import { request } from 'node:https'; | ||
|
||
/** | ||
* Core directory paths. | ||
* @property {string} rootDir | ||
* @property {string} srcDir | ||
* @property {string} gameSupportDir | ||
* @property {string} summaryFile | ||
* @property {string} gamesJsonFile | ||
* @readonly | ||
*/ | ||
export const CORE_PATHS = { | ||
rootDir: resolve(getDirName(), '..'), | ||
srcDir: resolve(getDirName(), '..', 'src'), | ||
gameSupportDir: resolve(getDirName(), '..', 'src', 'game-support'), | ||
summaryFile: resolve(getDirName(), '..', 'src', 'SUMMARY.md'), | ||
gamesJsonFile: resolve(getDirName(), '..', 'src', 'games.json') | ||
}; | ||
|
||
export const SCRIPT_GENERATE_START = '<!-- script:Generate Start -->'; | ||
export const SCRIPT_GENERATE_END = '<!-- script:Generate End -->'; | ||
|
||
/** | ||
* Gets the start and end sections of a file. | ||
* @param {string} content | ||
* @returns {[[number, number], null] | [null, 'not-found' | 'invalid-position']} | ||
*/ | ||
export const sectionsGetStartAndEnd = content => { | ||
// The start and end sections both need to be present | ||
const startMatch = content.indexOf(SCRIPT_GENERATE_START); | ||
const endMatch = content.indexOf(SCRIPT_GENERATE_END); | ||
if (startMatch === -1 || endMatch === -1) { | ||
logging.debug('Failed to find start or end section in file.'); | ||
return [null, 'not-found']; | ||
} | ||
|
||
// The end section must come after the start section | ||
if (startMatch > endMatch) { | ||
logging.debug('End section comes before start section in file.'); | ||
return [null, 'invalid-position']; | ||
} | ||
|
||
// Get the start and end sections | ||
return [[startMatch, endMatch], null]; | ||
}; | ||
|
||
export const TITLES_REGEX = /^# (.+)/; | ||
|
||
/** | ||
* Gets the title of a file. | ||
* @param {string} content | ||
* @returns {[string, null] | [null, 'not-found']} | ||
*/ | ||
export const getTitle = content => { | ||
// Match the title | ||
const titleMatch = content.match(TITLES_REGEX); | ||
if (!titleMatch || titleMatch.length < 2) { | ||
logging.debug('Failed to find title in file.'); | ||
return [null, 'not-found']; | ||
} | ||
|
||
return [titleMatch[1], null]; | ||
}; | ||
|
||
export const SCRIPT_ALIASES_REGEX = /<!-- script:Aliases ([\s\S]+?) -->/; | ||
|
||
/** | ||
* Parse aliases from a file. | ||
* @param {string} content | ||
* @returns {[string[], null] | [null, 'not-found' | 'bad-json' | 'bad-json-format']} | ||
*/ | ||
export const parseAliases = content => { | ||
// Match the aliases section | ||
const aliasesMatch = content.match(SCRIPT_ALIASES_REGEX); | ||
if (!aliasesMatch || aliasesMatch.length < 2) { | ||
logging.debug('Failed to find aliases section in file.'); | ||
return [null, 'not-found']; | ||
} | ||
|
||
// Parse the aliases | ||
let [aliasesParsed, aliasesError] = (() => { | ||
try { | ||
return [JSON.parse(aliasesMatch[1]), null]; | ||
} catch (error) { | ||
logging.debug('Failed to parse aliases section in file: %o', error); | ||
return [null, error]; | ||
} | ||
})(); | ||
if (aliasesError) { | ||
return [null, 'bad-json']; | ||
} | ||
if ( | ||
!aliasesParsed || | ||
!Array.isArray(aliasesParsed) || | ||
!aliasesParsed.every(alias => typeof alias === 'string') | ||
) { | ||
logging.debug( | ||
'Failed to parse aliases section in file: not an array of strings.' | ||
); | ||
return [null, 'bad-json-format']; | ||
} | ||
|
||
return [aliasesParsed, null]; | ||
}; | ||
|
||
export const REVIEW_METADATA_REGEX = | ||
/{{#template \.\.\/templates\/rating.md status=(Platinum|Gold|Silver|Bronze|Garbage) installs=(Yes|No) opens=(Yes|No)}}/; | ||
|
||
/** | ||
* @typedef {'Platinum' | 'Gold' | 'Silver' | 'Bronze' | 'Garbage'} RatingStatus | ||
*/ | ||
|
||
/** | ||
* Parse rating information from a file. | ||
* @param {string} content | ||
* @returns {[{ | ||
* status: RatingStatus, | ||
* installs: 'Yes' | 'No', | ||
* opens: 'Yes' | 'No', | ||
* }, null] | [null, 'not-found'] | ||
*/ | ||
export const parseReviewMetadata = content => { | ||
// Match the rating section | ||
const ratingMatch = content.match(REVIEW_METADATA_REGEX); | ||
if (!ratingMatch || ratingMatch.length < 4) { | ||
logging.debug('Failed to find rating section in file.'); | ||
return [null, 'not-found']; | ||
} | ||
|
||
const status = ratingMatch[1]; | ||
const installs = ratingMatch[2]; | ||
const opens = ratingMatch[3]; | ||
|
||
return [ | ||
{ | ||
status, | ||
installs, | ||
opens | ||
}, | ||
null | ||
]; | ||
}; | ||
|
||
export const GAMES_EMBEDS_METADATA = { | ||
steam: /{{#template ..\/templates\/steam.md id=(\d+)}}/ | ||
}; | ||
|
||
/** | ||
* @typedef {{ | ||
* type: 'steam', | ||
* id: number, | ||
* }} GameEmbed | ||
*/ | ||
|
||
/** | ||
* Get game embeds from a file. | ||
* @param {string} content | ||
* @returns {[[GameEmbed, number] | null, 'not-found' | 'multiple-found']} | ||
* | ||
*/ | ||
export const parseGameEmbeds = content => { | ||
// Match the game embeds section | ||
/** | ||
* @type {{ | ||
* location: number, | ||
* embed: GameEmbed | ||
* }[]} | ||
*/ | ||
const embeds = []; | ||
for (const [type, regex] of Object.entries(GAMES_EMBEDS_METADATA)) { | ||
const match = content.match(regex); | ||
if (match && match.length > 1) { | ||
embeds.push({ | ||
location: match.index, | ||
embed: { | ||
type, | ||
id: parseInt(match[1]) | ||
} | ||
}); | ||
} | ||
} | ||
|
||
if (embeds.length === 0) { | ||
logging.debug('Failed to find game embeds section in file.'); | ||
return [null, 'not-found']; | ||
} | ||
if (embeds.length > 1) { | ||
logging.debug('Found multiple game embeds section in file.'); | ||
return [null, 'multiple-found']; | ||
} | ||
|
||
return [[embeds[0].embed, embeds[0].location], null]; | ||
}; | ||
|
||
/** | ||
* Use webservers to check that a GameEmbed is valid. | ||
* @param {GameEmbed} embed | ||
* @returns {Promise<[boolean, null] | [null, 'invalid-embed' | 'web-request-failed']>} | ||
*/ | ||
export const checkGameEmbed = async embed => { | ||
if (embed.type === 'steam') { | ||
const steamUrl = | ||
'https://store.steampowered.com/app/' + | ||
encodeURIComponent(embed.id); | ||
const url = new URL(steamUrl); | ||
/** | ||
* @type {import('http').IncomingMessage} | ||
*/ | ||
const [response, responseError] = await new Promise(resolve => { | ||
request( | ||
{ | ||
hostname: url.hostname, | ||
port: 443, | ||
path: url.pathname, | ||
method: 'GET', | ||
headers: { | ||
'User-Agent': 'WhiskyBookBot/1.0' | ||
} | ||
}, | ||
resolve | ||
).end(); | ||
}) | ||
.then(response => [response, null]) | ||
.catch(error => [null, error]); | ||
if (responseError) { | ||
logging.debug('Failed to request Steam URL: %o', responseError); | ||
return [null, 'web-request-failed']; | ||
} | ||
|
||
if (response.statusCode === 200) { | ||
return [true, null]; | ||
} | ||
} | ||
|
||
return [false, 'invalid-embed']; | ||
}; | ||
|
||
const FILES_SKIP = ['README.md', 'template.md']; | ||
|
||
/** | ||
* Gets all markdown files in the game-support directory. | ||
* @returns {Promise<[string[], null] | [null, 'failed-to-read-dir']>} | ||
*/ | ||
export const getMarkdownFiles = async () => { | ||
const [gameSupportDirFiles, gameSupportDirFilesError] = await readdir( | ||
CORE_PATHS.gameSupportDir, | ||
{ withFileTypes: true } | ||
) | ||
.then(files => [files, null]) | ||
.catch(error => [null, error]); | ||
if (gameSupportDirFilesError) { | ||
logging.error( | ||
'Failed to read game-support directory: %o', | ||
gameSupportDirFilesError | ||
); | ||
return [null, 'failed-to-read-dir']; | ||
} | ||
|
||
return [ | ||
gameSupportDirFiles | ||
.filter( | ||
file => | ||
file.isFile() && | ||
file.name.endsWith('.md') && | ||
!FILES_SKIP.includes(file.name) | ||
) | ||
.map(file => resolve(CORE_PATHS.gameSupportDir, file.name)), | ||
null | ||
]; | ||
}; |
Oops, something went wrong.