Skip to content

Commit

Permalink
feat: Search (#10)
Browse files Browse the repository at this point in the history
You can now search the docs by pressing Ctrl+K in the docs.

Co-authored-by: Ludwig Richter <richter@fliegwerk.com>
  • Loading branch information
pklaschka and Ludwig Richter committed Jan 11, 2021
1 parent 548ea13 commit 0a3c342
Show file tree
Hide file tree
Showing 15 changed files with 225 additions and 27 deletions.
15 changes: 14 additions & 1 deletion src/model/tree.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
/**
* A documentation-ready tree for a single module
*/
export type ModuleTree = (Record<string, unknown> & { name: string })[];
export type ModuleTree = (Record<string, unknown> & {
name: string;
declarations: ModuleTreeNode[];
})[];

/**
* A node in a {@link ModuleTree}
*/
export type ModuleTreeNode<T = unknown> = {
type: string;
name: string;
declarations: Array<T>;
[key: string]: unknown;
};

/**
* A documentation-ready tree for multiple modules
Expand Down
7 changes: 4 additions & 3 deletions src/processors/class.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ClassDeclaration } from 'ts-morph';
import { ClassDeclaration, ClassDeclarationStructure } from 'ts-morph';
import { processJsDocs } from './helpers/processJsDocs';
import { extractPropertiesAndMethods } from './helpers/extractPropertiesAndMethods';
import { getConfig } from '../model/config';
import { ModuleTreeNode } from '../model';

/**
* Converts a `ClassDeclaration` or `InterfaceDeclaration` to a documentation-ready representation.
Expand All @@ -15,7 +16,7 @@ import { getConfig } from '../model/config';
*/
export function processClassDeclaration(
node: ClassDeclaration
): Record<string, unknown> {
): ModuleTreeNode<ClassDeclarationStructure> {
const structure = node.getStructure();
extractPropertiesAndMethods(structure, node);

Expand All @@ -40,7 +41,7 @@ export function processClassDeclaration(

return {
type: 'class',
name: node.getName(),
name: node.getName() ?? '[[UnnamedClass]]',
declarations: [structure]
};
}
5 changes: 3 additions & 2 deletions src/processors/function.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FunctionDeclaration } from 'ts-morph';
import { processJsDocs } from './helpers/processJsDocs';
import { ModuleTreeNode } from '../model';

/**
* Converts a `FunctionDeclaration` to a documentation-ready representation.
Expand All @@ -13,11 +14,11 @@ import { processJsDocs } from './helpers/processJsDocs';
*/
export function processFunctionDeclaration(
node: FunctionDeclaration
): Record<string, unknown> {
): ModuleTreeNode {
// return signature.getDocumentationComments()
return {
type: 'function',
name: node.getName(),
name: node.getName() || '[[unnamedFunction]]',
declarations: [
{
...node.getStructure(),
Expand Down
7 changes: 5 additions & 2 deletions src/processors/identifier.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Identifier } from 'ts-morph';
import { processNode } from './init';
import { ModuleTreeNode } from '../model';

/**
* Converts an `Identifier` to a documentation-ready representation.
Expand All @@ -11,10 +12,12 @@ import { processNode } from './init';
* processIdentifier(node);
* ```
*/
export function processIdentifier(node: Identifier): Record<string, unknown> {
export function processIdentifier(
node: Identifier
): ModuleTreeNode<ModuleTreeNode> {
return {
type: 'identifier',
name: node.getText(),
implementations: node.getDefinitionNodes().map(processNode)
declarations: node.getDefinitionNodes().map(processNode)
};
}
9 changes: 7 additions & 2 deletions src/processors/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { processVariableDeclaration } from './variable';
import { processIdentifier } from './identifier';
import { processModule } from './module';
import { processInterfaceDeclaration } from './interface';
import { ModuleTreeNode } from '../model';

/**
* Processes a node for documentation-relevant values
Expand All @@ -27,7 +28,7 @@ import { processInterfaceDeclaration } from './interface';
* processNode(node);
* ```
*/
export function processNode(node: Node): Record<string, unknown> {
export function processNode(node: Node): ModuleTreeNode {
switch (node.getKind()) {
case SyntaxKind.FunctionDeclaration:
return processFunctionDeclaration(node as FunctionDeclaration);
Expand All @@ -44,6 +45,10 @@ export function processNode(node: Node): Record<string, unknown> {
case SyntaxKind.ModuleDeclaration:
return processModule(node as NamespaceDeclaration);
default:
return { type: 'unknown', kind: node.getKindName() };
return {
type: 'unknown::' + node.getKindName(),
declarations: [],
name: node.getSymbol()?.getName() ?? 'unknown'
};
}
}
5 changes: 3 additions & 2 deletions src/processors/interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InterfaceDeclaration } from 'ts-morph';
import { InterfaceDeclaration, InterfaceDeclarationStructure } from 'ts-morph';
import { extractPropertiesAndMethods } from './helpers/extractPropertiesAndMethods';
import { ModuleTreeNode } from '../model';

/**
* Converts a `ClassDeclaration` or `InterfaceDeclaration` to a documentation-ready representation.
Expand All @@ -13,7 +14,7 @@ import { extractPropertiesAndMethods } from './helpers/extractPropertiesAndMetho
*/
export function processInterfaceDeclaration(
node: InterfaceDeclaration
): Record<string, unknown> {
): ModuleTreeNode<InterfaceDeclarationStructure> {
const structure = node.getStructure();
extractPropertiesAndMethods(structure, node);
return {
Expand Down
16 changes: 7 additions & 9 deletions src/processors/module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NamespaceDeclaration } from 'ts-morph';
import { processNode } from './init';
import { processJsDocs } from './helpers/processJsDocs';
import { ModuleTreeNode } from '../model';

/**
* Converts a `NamespaceDeclaration` to a documentation-ready representation.
Expand All @@ -14,22 +15,19 @@ import { processJsDocs } from './helpers/processJsDocs';
*/
export function processModule(
node: NamespaceDeclaration
): Record<string, unknown> {
): ModuleTreeNode<ModuleTreeNode> {
if (node.hasNamespaceKeyword()) {
const res: Record<string, unknown> & { exportedMembers: unknown[] } = {
const res: ModuleTreeNode<ModuleTreeNode> = {
name: node.getName(),
docs: processJsDocs(node.getJsDocs()),
type: 'namespace',
exportedMembers: []
declarations: []
};
for (const [name, declarations] of node.getExportedDeclarations()) {
res.exportedMembers.push({
name,
declarations: declarations.map(processNode)
});
for (const [, declarations] of node.getExportedDeclarations()) {
res.declarations.push(...declarations.map(processNode));
}
return res;
}
// TODO: Module type
return { type: 'module' };
return { type: 'module', name: node.getName(), declarations: [] };
}
7 changes: 3 additions & 4 deletions src/processors/type.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { TypeAliasDeclaration } from 'ts-morph';
import { processJsDocs } from './helpers/processJsDocs';
import { ModuleTreeNode } from '../model';

/**
* Converts a `TypeAleasDeclaration` to a documentation-ready representation.
* Converts a `TypeAliasDeclaration` to a documentation-ready representation.
*
* @param node - the type alias declaration
* @returns documentation-ready representation of the type
Expand All @@ -11,9 +12,7 @@ import { processJsDocs } from './helpers/processJsDocs';
* processType(node);
* ```
*/
export function processType(
node: TypeAliasDeclaration
): Record<string, unknown> {
export function processType(node: TypeAliasDeclaration): ModuleTreeNode {
return {
type: 'type',
name: node.getName(),
Expand Down
3 changes: 2 additions & 1 deletion src/processors/variable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { VariableDeclaration } from 'ts-morph';
import { processJsDocs } from './helpers/processJsDocs';
import { ModuleTreeNode } from '../model';

/**
* Converts a `VariableDeclaration` to a documentation-ready representation.
Expand All @@ -13,7 +14,7 @@ import { processJsDocs } from './helpers/processJsDocs';
*/
export function processVariableDeclaration(
node: VariableDeclaration
): Record<string, unknown> {
): ModuleTreeNode {
return {
type: 'variable',
name: node.getName(),
Expand Down
8 changes: 8 additions & 0 deletions src/themes/html/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { renderFile } from 'eta';
import { getConfig } from '../../model/config';
import { getSearchIndex } from './serach-index';

const origMd = new MarkdownIt({ linkify: true });
const viewFolder = path.resolve(__dirname, '..', '..', '..', 'views');
Expand Down Expand Up @@ -108,6 +109,13 @@ export const HTMLTheme: Theme = {
})
);

const searchIndex = getSearchIndex(tree, getConfig());
await createFile(
path.join(getConfig().outDir, 'search-index.json'),
Buffer.from(JSON.stringify(searchIndex)),
'.json'
);

// render readme content to index.html
await render(
'plain',
Expand Down
126 changes: 126 additions & 0 deletions src/themes/html/serach-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Tree, ModuleTreeNode, FliegdocConfig } from '../../model';
import {
ClassDeclarationStructure,
InterfaceDeclarationStructure
} from 'ts-morph';

/**
* Creates a search index for the passed {@link Tree}
*
* @param tree - the tree for which the index gets generated
* @param config - the config that gets used
* @returns the search index, containing all linked members of the docs
*
* @example
* ```ts
* console.log(getSearchIndex(tree))
* ```
*/
export function getSearchIndex(
tree: Tree,
config: FliegdocConfig
): SearchResult[] {
const res: SearchResult[] = [];

Object.keys(tree).forEach(moduleName => {
res.push({
name: moduleName,
text: moduleName,
url: `${config.baseUrl}${moduleName}`
});
const moduleTree = tree[moduleName];
// noinspection DuplicatedCode
moduleTree.forEach(moduleMember => {
res.push({
name: moduleMember.name,
text: moduleName + '.' + moduleMember.name,
url: `${config.baseUrl}${moduleName}#${moduleMember.name}`
});

for (const node of moduleMember.declarations) {
if (isClass(node)) {
const classDeclaration = node.declarations[0];
classDeclaration.properties?.forEach(property => {
res.push({
name: property.name,
text: `${moduleName}.${moduleMember.name}.${property.name}`,
url: `${config.baseUrl}${moduleName}#${moduleMember.name}.${property.name}`
});
});
classDeclaration.methods?.forEach(method => {
res.push({
name: method.name,
text: `${moduleName}.${moduleMember.name}.${method.name}`,
url: `${config.baseUrl}${moduleName}#${moduleMember.name}.${method.name}`
});
});
} else if (isInterface(node)) {
const interfaceDeclaration = node.declarations[0];
interfaceDeclaration.properties?.forEach(property => {
res.push({
name: property.name,
text: `${moduleName}.${moduleMember.name}.${property.name}`,
url: `${config.baseUrl}${moduleName}#${moduleMember.name}.${property.name}`
});
});
interfaceDeclaration.methods?.forEach(method => {
res.push({
name: method.name,
text: `${moduleName}.${moduleMember.name}.${method.name}`,
url: `${config.baseUrl}${moduleName}#${moduleMember.name}.${method.name}`
});
});
}
}
});
});

return res;
}

/**
* Checks if the given node is a {@link ModuleTreeNode} contains a class declaration
*
* @param node - the node
* @returns does `node.declarations` contain a class declaration?
*
* @example
* ```ts
* if (isClass(node)) {
* // node.declarations[0] is a ClassDeclarationStructure
* }
* ```
*/
function isClass(
node: ModuleTreeNode
): node is ModuleTreeNode<ClassDeclarationStructure> {
return node.type === 'class';
}

/**
* Checks if the given node is a {@link ModuleTreeNode} contains an interface declaration
*
* @param node - the node
* @returns does `node.declarations` contain an interface declaration?
*
* @example
* ```ts
* if (isInterface(node)) {
* // node.declarations[0] is an InterfaceDeclarationStructure
* }
* ```
*/
function isInterface(
node: ModuleTreeNode
): node is ModuleTreeNode<InterfaceDeclarationStructure> {
return node.type === 'interface';
}

/**
* A search result.
*/
export interface SearchResult {
name: string;
url: string;
text: string;
}
3 changes: 2 additions & 1 deletion views/layout/footer.eta
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
</div>
</div>
</div>
<%~includeFile('layout/scripts', it)%>
</body>
</footer>
</html>
11 changes: 11 additions & 0 deletions views/layout/header.eta
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ const {config, modules} = it;
:root {
scroll-behavior: smooth;
}

header {
display: flex;
align-items: center;
}

.spacer {
flex-grow: 1;
}
</style>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
Expand All @@ -27,6 +36,8 @@ const {config, modules} = it;
<% Object.keys(config.externalLinks).forEach(function (name) { %>
<a class="button" href="<%=config.externalLinks[name] %>" target="_blank"><%=name%></a>
<% }) %>
<div class="spacer"></div>
<button onclick="openSearch()"><span class="icon-search"></span> Search: <kbd>Ctrl</kbd>+<kbd>K</kbd></button>
</header>
<div class="container">
<div class="row">
Expand Down
Loading

0 comments on commit 0a3c342

Please sign in to comment.