Skip to content

Commit

Permalink
Whoops, forgot to commit (I hope this gets squashed)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaBrest committed May 12, 2024
1 parent 4d013df commit e542c7a
Show file tree
Hide file tree
Showing 95 changed files with 1,251 additions and 465 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/lint.yml
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
3 changes: 2 additions & 1 deletion .github/workflows/mdbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ jobs:
env:
MDBOOK_VERSION: 0.4.37
CARGO_TERM_COLOR: always
GITHUB_ACTIONS: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- name: Generate summary
run: node scripts/generate-summary.mjs
run: node scripts/generate.mjs
- uses: Swatinem/rust-cache@v2
- name: Install mdBook
run: |
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,12 @@ Documentation for Whisky.
```
<img width="815" alt="Screenshot 2024-04-16 at 10 06 11 PM" src="https://github.com/Whisky-App/whisky-book/assets/161992562/d7d61b1a-5d02-4961-8ff5-b953c2a2fbe1">
3. Run the `generate-summary` script with `./scripts/generate-summary.mjs` to update the `SUMMARY.md` and `game-support/README.md` files.
3. Run the `generate` script with `./scripts/generate.mjs` to update `SUMMARY.md`.
This will also make the game appear in the sidebar of the book.
4. Create a pull request detailing the changes you made. Ensure that it's consise, yet readable and coherent.
- You will need to create a fork of `whisky-book` and push your changes there before creating a PR. Once you've done that, then you can submit a PR to merge your fork with `main`.
5. Sit back, wait for PR reviews, and make changes as necessary.
5. Run `./scripts/lint.mjs` to ensure that your changes are properly formatted.
6. Sit back, wait for PR reviews, and make changes as necessary.

Have any questions about this process or anything Whisky-related? Stop by the [Discord](https://discord.gg/CsqAfs9CnM) and ask us a question! We're more than happy to help.

Expand Down
7 changes: 7 additions & 0 deletions scripts/.prettierrc.yaml
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
274 changes: 274 additions & 0 deletions scripts/core.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
/**
* @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'];
}

return [response.statusCode === 200, 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
];
};
Loading

0 comments on commit e542c7a

Please sign in to comment.