Skip to content

Commit

Permalink
feat(web app): add Table Of Contents
Browse files Browse the repository at this point in the history
- improve content interfaces
  • Loading branch information
inkwell-studio committed Jul 28, 2023
1 parent 4c48a91 commit 3dc6356
Show file tree
Hide file tree
Showing 65 changed files with 89,420 additions and 17,660 deletions.
17 changes: 16 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,26 @@
],
"attachSimplePort": 9229
},
{
"request": "launch",
"name": "Artifacts",
"type": "node",
"program": "${workspaceFolder}/catechism/artifact-builders/build.ts",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "deno",
"runtimeArgs": [
"run",
"--inspect-brk",
"--allow-read",
"--allow-write"
],
"attachSimplePort": 9229
},
{
"request": "launch",
"name": "Mock Data",
"type": "node",
"program": "${workspaceFolder}/catechism/mock-data/build.ts",
"program": "${workspaceFolder}/catechism/mock-data/build/build.ts",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "deno",
"runtimeArgs": [
Expand Down
55 changes: 43 additions & 12 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
# Tasks

- [ ] rebuild the table-of-contents generation algorithm:
- [ ] the algorithm:
- [ ] for the four parts:
- [ ] all populated `openingContent` arrays are an entry (referenced by the parent)
- [ ] all `mainContent` arrays are split by `InBrief` content; entries are created for each sub-array (referenced
by the first element in the sub-array)
- [ ] write tests to verify that all content is accessible via the generated table-of-contents
- [ ] determine if the semantic-path-to-path-id map needs refactoring

- [ ] investigate "TODO" note above `utils::hasChildContent()`
- [ ] investigate `utils::getOpeningAndMainContent()`: should the return type be `ContentBase` instead of `T`?
- [ ] implement proper content routing/rendering

## Rendering content

| content type selected | content type loaded and rendered |
| --------------------- | --------------------------------- |
| `Prologue` | Prologue |
| `Part` | `openingContent` and first child |
| `Section` | `openingContent` and first child |
| `Chapter` | `openingContent` and first child |
| `Article` | see _Rendering Article_ |
| `Article Paragraph` | see _Rendering Article Paragraph_ |
| all else | see _Rendering low-level content_ |

### Rendering `Article`

- If `mainContent` contains any `ArticleParagraph`s:
- render `openingContent` and `mainContent[0]`
- Else:
- render entire element

### Rendering `Article Paragraph`

- If it is the first child of its parent:
- render its parent according to its rule
- Else:
- render entire element

### Rendering low-level content

- Render the nearest ancestor of the following types according to its rule:
- `Prologue`
- `Part`
- `Section`
- `Chapter`
- `Article`
- `Article Paragraph`

---

- [ ] determine if the semantic-path-to-path-id map needs refactoring
- [ ] use `.json` files instead of "hand-built" `.ts` files (e.g. `catechism.ts`)

- [ ] render all content
- [ ] re-do the rendering structures (reorganize components, etc.)
Expand Down Expand Up @@ -51,7 +82,7 @@

# Possible features

- [ ] the ability to ask a question in natural language (via text or mic), e.g. "What happens in the sacrament of
- [ ] the ability to ask a question in natural language (via text or mic), e.g. "What happens in the Sacrament of
Confirmation?"
- [ ] note-taking and highlighting
- [ ] permanent and temporary storage (easily toggleable)
Expand Down
3 changes: 2 additions & 1 deletion catechism/artifact-builders/build.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { build as buildPathMap } from './path-map.ts';
import { build as buildTableOfContents } from './table-of-contents.ts';
import { Catechism } from '../source/catechism.ts';
import { PathMap, TableOfContentsType } from '../source/types/types.ts';

buildArtifacts();
Expand All @@ -8,7 +9,7 @@ function buildArtifacts(): void {
console.log('\nBuilding artifacts ...');

console.log('\ttable-of-contents ...');
const tableOfContents = buildTableOfContents();
const tableOfContents = buildTableOfContents(Catechism);
writeJson(tableOfContents, 'table-of-contents');

console.log('\tsemantic-path to path-id map ...');
Expand Down
144 changes: 63 additions & 81 deletions catechism/artifact-builders/table-of-contents.ts
Original file line number Diff line number Diff line change
@@ -1,131 +1,106 @@
import { Catechism } from '../source/catechism.ts';
import { ArticleParagraph } from '../source/types/article-paragraph.ts';
import { Subarticle } from '../source/types/subarticle.ts';
import { CatechismStructure } from '../source/types/catechism-structure.ts';
import {
Article,
ArticleParagraph,
buildSemanticPath,
Chapter,
Content,
ContentBase,
ContentContainer,
Paragraph,
ParagraphGroup,
Part,
Section,
SemanticPathSource,
Subarticle,
TableOfContentsEntry,
TableOfContentsType,
} from '../source/types/types.ts';
import { getParagraphs, hasMainContent, hasOpeningContent } from '../utils.ts';
import { ParagraphGroup } from '../source/types/paragraph-group.ts';
import { getInBrief, getMainContent, getParagraphs } from '../utils.ts';

//#region builders
export function build(): TableOfContentsType {
export function build(catechism: CatechismStructure): TableOfContentsType {
return {
prologue: buildEntry(Catechism.prologue, [], true),
parts: Catechism.parts.map((part) => buildEntry(part, [])),
prologue: buildEntry(catechism.prologue, [], true),
parts: catechism.parts.map((part) => buildEntry(part, [])),
};
}

function buildEntry<T extends ContentBase | ContentBase & ContentContainer>(
content: T,
function buildEntry(
content: ContentBase | ContentContainer,
ancestors: Array<SemanticPathSource>,
ignoreChildren = false,
forceIncludeChildren = false,
): TableOfContentsEntry {
const firstParagraphNumber = getFirstParagraphNumber(content);
if (typeof firstParagraphNumber !== 'number') {
throw Error(
`A paragraph could not be found for the given content: ${content.contentType}, pathID: ${content.pathID}`,
);
}

const semanticPathSource = getSemanticPathSource(content);
const { firstParagraphNumber, lastParagraphNumber } = getTerminalParagraphNumbers(content);

return {
contentType: content.contentType,
title: getTitle(content),
pathID: content.pathID,
semanticPath: buildSemanticPath(semanticPathSource, ancestors),
firstParagraphNumber,
children: ignoreChildren ? [] : buildChildEntries(content, [semanticPathSource, ...ancestors]),
lastParagraphNumber,
children: buildChildEntries(content, [...ancestors, semanticPathSource], forceIncludeChildren),
};
}

function buildChildEntries<T extends ContentBase | ContentBase & ContentContainer>(
content: T,
function buildChildEntries(
parent: ContentBase | ContentContainer,
ancestors: Array<SemanticPathSource>,
forceIncludeChildren = false,
): Array<TableOfContentsEntry> {
const childEntries: Array<TableOfContentsEntry> = [];

const hasPopulatedOpeningContent = hasOpeningContent(content) &&
(content as ContentContainer).openingContent.length > 0;

if (hasPopulatedOpeningContent) {
// TODO: Implement
/*/
const openingContentRoot = (content as ContentContainer).openingContent[0];
// TODO: Which is correct? (Should `openingContentRoot` or `content` be used?)
const openingContentEntry: TableOfContentsEntry = {
contentType: openingContentRoot.contentType,
title: 'Opening Content',
pathID: openingContentRoot.pathID,
semanticPath: TODO,
firstParagraphNumber: TODO,
children: []
};
const childEntries = getMainContent(parent)
.filter((child) => forceIncludeChildren || shouldGenerateChildEntry(parent, child))
.map((child) => buildEntry(child, ancestors));

const openingContentEntry: TableOfContentsEntry = {
contentType: content.contentType,
title: 'Opening Content',
pathID: content.pathID,
semanticPath: TODO,
firstParagraphNumber: TODO,
children: []
};
childEntries.push(openingContentEntry);
/*/
const inBrief = getInBrief(parent);
if (inBrief) {
childEntries.push(buildEntry(inBrief, ancestors));
}

const mainContentEntries = hasMainContent(content)
? (content as ContentContainer).mainContent
.filter((content) => includeInTableOfContents(content))
.map((child) => buildEntry(child, ancestors))
: [];

return childEntries.concat(mainContentEntries);
return childEntries;
}
//#endregion

//#region helpers
/**
* @returns `true` if the content should be included in the Table of Contents, and `false` otherwise
* @returns `true` if a table-of-contents entry should be generated for the provided parent-child pairing, and `false` otherwise
*/
function includeInTableOfContents<T extends ContentBase>(content: T): boolean {
return Content.PROLOGUE === content.contentType ||
Content.PART === content.contentType ||
Content.SECTION === content.contentType ||
Content.CHAPTER === content.contentType ||
Content.ARTICLE === content.contentType ||
Content.ARTICLE_PARAGRAPH === content.contentType ||
Content.SUB_ARTICLE === content.contentType ||
Content.IN_BRIEF === content.contentType;
function shouldGenerateChildEntry(parent: ContentBase, child: ContentBase): boolean {
// A list of [parent, child] pairings that will trigger the generation of a table-of-contents entry for the child
return [
[Content.PART, Content.SECTION],
[Content.SECTION, Content.CHAPTER],
[Content.SECTION, Content.ARTICLE],
[Content.CHAPTER, Content.ARTICLE],
[Content.CHAPTER, Content.SUB_ARTICLE],
[Content.ARTICLE, Content.ARTICLE_PARAGRAPH],
[Content.ARTICLE, Content.SUB_ARTICLE],
].some((validPairing) => parent.contentType === validPairing[0] && child.contentType === validPairing[1]);
}

function getTitle(content: ContentBase): string {
return `${Content[content.contentType]} ${content.pathID}`;
const number = getSemanticPathSource(content).number;
const numberSuffix = number ? ` ${number}` : '';

// Replace underscores with spaces and implement title-casing
return `${Content[content.contentType]}${numberSuffix}`
.replaceAll('SUB_ARTICLE', 'SUBARTICLE')
.toLowerCase()
.split('_')
.map((part) => part.substring(0, 1).toUpperCase() + part.substring(1))
.join(' ');
}

function getSemanticPathSource<T extends ContentBase>(
content: T,
): SemanticPathSource {
function getSemanticPathSource(content: ContentBase): SemanticPathSource {
return {
content: content.contentType,
number: getNumber(content),
};
}

function getNumber<T extends ContentBase>(content: T): number | null {
function getNumber(content: ContentBase): number | null {
if (Content.PART === content.contentType) {
return (content as unknown as Part).partNumber;
} else if (Content.SECTION === content.contentType) {
Expand All @@ -148,15 +123,22 @@ function getNumber<T extends ContentBase>(content: T): number | null {
}

/**
* @returns the `paragraphNumber` value of the first `Paragraph` that's a child of the given content, or `null` if no such `Paragraph` exists
* @returns the first and last `paragraphNumber` values of all the `Paragraph`s contained by the provided content
*/
function getFirstParagraphNumber<T extends ContentBase | ContentBase & ContentContainer>(content: T): number | null {
const mainContentExists = hasMainContent(content);
if (mainContentExists) {
const paragraphs = getParagraphs([content as ContentBase & ContentContainer]);
return paragraphs[0]?.paragraphNumber ?? null;
} else {
return null;
function getTerminalParagraphNumbers(
content: ContentBase,
): { firstParagraphNumber: number; lastParagraphNumber: number } {
const paragraphs = getParagraphs([content]);
const firstParagraphNumber = paragraphs.at(0)?.paragraphNumber ?? null;
const lastParagraphNumber = paragraphs.at(-1)?.paragraphNumber ?? null;

if (firstParagraphNumber === null || lastParagraphNumber === null) {
throw new Error(
`A terminal paragraph could not be found for the given content: ${content.contentType}, pathID: ${content.pathID}. ` +
`Terminal paragraph numbers: ${firstParagraphNumber}, ${lastParagraphNumber}`,
);
}

return { firstParagraphNumber, lastParagraphNumber };
}
//#endregion
3 changes: 2 additions & 1 deletion catechism/artifacts/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Excepting this file, all files in this directory are programmatically generated, and should not be manually modified.
Excepting this file and test files (i.e. `*.test.ts`), all files in this directory are programmatically generated, and
should not be manually modified.

0 comments on commit 3dc6356

Please sign in to comment.