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

Auto generate sidebar and index. #30

Merged
merged 20 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
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
6 changes: 6 additions & 0 deletions .github/workflows/mdbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +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.mjs
- uses: Swatinem/rust-cache@v2
- name: Install mdBook
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
book
.idea
**/.DS_Store
src/games.json
20 changes: 7 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ Documentation for Whisky.
cargo install mdbook-template
cargo install mdbook-last-changed
```
5. You can preview changes to the book with `mdbook serve --open`
5. Install [Node.js](https://nodejs.org/en/download/).
You can also install it through [Homebrew](https://brew.sh/) with `brew install node`.
6. You can preview changes to the book with `mdbook serve --open`

### Adding Game Pages:
0. Standards to uphold:
Expand Down Expand Up @@ -69,19 +71,11 @@ 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. Add your game to `~/whisky-book/src/SUMMARY.md`. Make sure that you insert it in the proper alphabetical order, not deleting any games. Ensure the proper spacing and indentation is followed. Here is an example with [Diablo IV (Battle.net)](https://docs.getwhisky.app/game-support/diablo-4-battle-net.html)
```
...
- [Cyberpunk 2077](./game-support/cyberpunk-2077.md)
- [Dark Souls III](./game-support/dark-souls-3.md)
- [Diablo IV (Battle.net)](./game-support/diablo-4-battle-net.md)
- [Diablo IV (Steam)](./game-support/diablo-4-steam.md)
- [Dorfromantik](./game-support/dorfromantik.md)
...
```
4. Add your game to `~/whisky-book/src/game-support/README.md`. Follow the same standards as above.
5. Create a pull request detailing the changes you made. Ensure that it's consise, yet readable and coherent.
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. 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
276 changes: 276 additions & 0 deletions scripts/core.mjs
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
];
};
Loading