Compare commits

...

3 Commits
v6.3.1 ... docs

Author SHA1 Message Date
Olivier Guyot
2ad39cf69e Added missing Type component 2019-09-24 10:50:52 +02:00
Olivier Guyot
6929cb3001 Store typedef on the helper and use it to show a parameter list 2019-09-24 10:47:06 +02:00
Tim Schaub
5ee3063d01 Gatsby setup for API docs 2019-05-20 10:30:44 -06:00
21 changed files with 21393 additions and 1 deletions

View File

@@ -50,3 +50,11 @@ jobs:
- store_artifacts:
path: build/apidoc
destination: apidoc
- run:
name: Build Website
command: npm run build-site
- store_artifacts:
path: public
destination: website

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@
/coverage/
/dist/
node_modules/
/.cache/
/public/

View File

@@ -0,0 +1,23 @@
{
"opts": {
"recurse": true,
"template": "node_modules/jsdoc-json"
},
"tags": {
"allowUnknownTags": true
},
"source": {
"includePattern": "\\.js$",
"include": [
"src/ol"
]
},
"plugins": [
"jsdoc-plugin-typescript",
"config/jsdoc/api-info/plugins/api",
"config/jsdoc/api-info/plugins/module"
],
"typescript": {
"moduleRoot": "src"
}
}

View File

@@ -0,0 +1,15 @@
/**
* Handle the api annotation.
* @param {Object} dictionary The tag dictionary.
*/
exports.defineTags = dictionary => {
dictionary.defineTag('api', {
onTagged: (doclet, tag) => {
doclet.api = true;
}
});
};

View File

@@ -0,0 +1,170 @@
/**
* This plugin adds an `exportMap` property to @module doclets. Each export map
* is an object with properties named like the local identifier and values named
* like the exported identifier.
*
* For example, the code below
*
* export {foo as bar};
*
* would be a map like `{foo: 'bar'}`.
*
* In the case of an export declaration with a source, the export identifier is
* prefixed by the source. For example, this code
*
* export {foo as bar} from 'ol/bam';
*
* would be a map like `{'ol/bam foo': 'bar'}`.
*
* If a default export is a literal or object expression, the local name will be
* an empty string. For example
*
* export default {foo: 'bar'};
*
* would be a map like `{'': 'default'}`.
*/
const assert = require('assert');
const path = require('path');
/**
* A lookup of export maps per source filepath.
*/
const exportMapLookup = {};
function loc(filepath, node) {
return `${filepath}:${node.loc.start.line}`;
}
function nameFromChildIdentifier(filepath, node) {
assert.ok(node.id, `expected identifer in ${loc(filepath, node)}`);
assert.strictEqual(node.id.type, 'Identifier', `expected identifer in ${loc(filepath, node)}`);
return node.id.name;
}
function handleExportNamedDeclaration(filepath, node) {
if (!(filepath in exportMapLookup)) {
exportMapLookup[filepath] = {};
}
const exportMap = exportMapLookup[filepath];
const declaration = node.declaration;
if (declaration) {
// `export class Foo{}` or `export function foo() {}`
if (declaration.type === 'ClassDeclaration' || declaration.type === 'FunctionDeclaration') {
const name = nameFromChildIdentifier(filepath, declaration);
exportMap[name] = name;
return;
}
// `export const foo = 'bar', bam = 42`
if (declaration.type === 'VariableDeclaration') {
const declarations = declaration.declarations;
assert.ok(declarations.length > 0, `expected variable declarations in ${loc(filepath, declaration)}`);
for (const declarator of declarations) {
assert.strictEqual(declarator.type, 'VariableDeclarator', `unexpected "${declarator.type}" in ${loc(filepath, declarator)}`);
const name = nameFromChildIdentifier(filepath, declarator);
exportMap[name] = name;
}
return;
}
throw new Error(`Unexpected named export "${declaration.type}" in ${loc(filepath, declaration)}`);
}
let prefix = '';
const source = node.source;
if (source) {
// `export foo from 'bar'`
assert.strictEqual(source.type, 'Literal', `unexpected export source "${source.type}" in ${loc(filepath, source)}`);
prefix = `${source.value} `;
}
const specifiers = node.specifiers;
assert.ok(specifiers.length > 0, `expected export specifiers in ${loc(filepath, node)}`);
// `export {foo, bar}` or `export {default as Foo} from 'bar'`
for (const specifier of specifiers) {
assert.strictEqual(specifier.type, 'ExportSpecifier', `unexpected export specifier in ${loc(filepath, specifier)}`);
const local = specifier.local;
assert.strictEqual(local.type, 'Identifier', `unexpected local specifier "${local.type} in ${loc(filepath, local)}`);
const exported = specifier.exported;
assert.strictEqual(local.type, 'Identifier', `unexpected exported specifier "${exported.type} in ${loc(filepath, exported)}`);
exportMap[prefix + local.name] = exported.name;
}
}
function handleDefaultDeclaration(filepath, node) {
if (!(filepath in exportMapLookup)) {
exportMapLookup[filepath] = {};
}
const exportMap = exportMapLookup[filepath];
const declaration = node.declaration;
if (declaration) {
// `export default class Foo{}` or `export default function foo () {}`
if (declaration.type === 'ClassDeclaration' || declaration.type === 'FunctionDeclaration') {
const name = nameFromChildIdentifier(filepath, declaration);
exportMap[name] = 'default';
return;
}
// `export default foo`
if (declaration.type === 'Identifier') {
exportMap[declaration.name] = 'default';
return;
}
// `export default {foo: 'bar'}` or `export default 42`
if (declaration.type === 'ObjectExpression' || declaration.type === 'Literal') {
exportMap[''] = 'default';
return;
}
}
throw new Error(`Unexpected default export "${declaration.type}" in ${loc(filepath, declaration)}`);
}
exports.astNodeVisitor = {
visitNode: (node, event, parser, filepath) => {
if (node.type === 'ExportNamedDeclaration') {
return handleExportNamedDeclaration(filepath, node);
}
if (node.type === 'ExportDefaultDeclaration') {
return handleDefaultDeclaration(filepath, node);
}
}
};
const moduleLookup = {};
exports.handlers = {
// create a lookup of @module doclets
newDoclet: event => {
const doclet = event.doclet;
if (doclet.kind === 'module') {
const filepath = path.join(doclet.meta.path, doclet.meta.filename);
assert.ok(!(filepath in moduleLookup), `duplicate @module doc in ${filepath}`);
moduleLookup[filepath] = doclet;
}
},
// assign the `exportMap` property to @module doclets
parseComplete: event => {
for (const filepath in moduleLookup) {
assert.ok(filepath in exportMapLookup, `missing ${filepath} in export map lookup`);
moduleLookup[filepath].exportMap = exportMapLookup[filepath];
}
// make sure there was a @module doclet for each export map
for (const filepath in exportMapLookup) {
assert.ok(filepath in moduleLookup, `missing @module doclet in ${filepath}`);
}
}
};

11
gatsby-config.js Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
plugins: [
'gatsby-plugin-emotion',
{
resolve: 'gatsby-plugin-typography',
options: {
pathToConfigModule: 'site/util/typography'
}
}
]
};

23
gatsby-node.js Normal file
View File

@@ -0,0 +1,23 @@
function getDocs() {
// TODO: build if not present
const info = require('./build/api-info.json');
return info.docs.filter(doc => !doc.ignore && (doc.api || doc.kind === 'module' || doc.kind === 'typedef'));
}
function createPages({actions: {createPage}}) {
createPage({
path: '/api',
component: require.resolve('./site/pages/API.js'),
context: {docs: getDocs()}
});
createPage({
path: '/info',
component: require.resolve('./site/pages/Info.js'),
context: {docs: getDocs()}
});
}
exports.createPages = createPages;

20668
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,10 @@
"copy-css": "shx cp src/ol/ol.css build/ol/ol.css",
"transpile": "shx rm -rf build/ol && shx mkdir -p build/ol && shx cp -rf src/ol build/ol/src && node tasks/serialize-workers && tsc --project config/tsconfig-build.json",
"typecheck": "tsc --pretty",
"apidoc": "jsdoc -R config/jsdoc/api/index.md -c config/jsdoc/api/conf.json -P package.json -d build/apidoc"
"apidoc": "jsdoc -R config/jsdoc/api/index.md -c config/jsdoc/api/conf.json -P package.json -d build/apidoc",
"api-info": "jsdoc --configure config/jsdoc/api-info/conf.json --destination build/api-info.json",
"serve-site": "gatsby develop",
"build-site": "npm run api-info && gatsby build"
},
"main": "index.js",
"repository": {
@@ -43,6 +46,8 @@
"devDependencies": {
"@babel/core": "^7.4.0",
"@babel/preset-env": "^7.4.4",
"@emotion/core": "^10.0.10",
"@emotion/styled": "^10.0.11",
"@openlayers/eslint-plugin": "^4.0.0-beta.2",
"@types/arcgis-rest-api": "^10.4.4",
"@types/geojson": "^7946.0.7",
@@ -57,9 +62,14 @@
"coveralls": "3.0.3",
"eslint": "^5.16.0",
"eslint-config-openlayers": "^11.0.0",
"eslint-config-tschaub": "^13.1.0",
"eslint-plugin-react": "^7.13.0",
"expect.js": "0.3.1",
"front-matter": "^3.0.2",
"fs-extra": "^8.0.0",
"gatsby": "^2.4.3",
"gatsby-plugin-emotion": "^4.0.6",
"gatsby-plugin-typography": "^2.2.13",
"glob": "^7.1.4",
"globby": "^9.2.0",
"handlebars": "4.1.2",
@@ -68,6 +78,7 @@
"istanbul-instrumenter-loader": "^3.0.1",
"jquery": "3.4.1",
"jsdoc": "3.6.2",
"jsdoc-json": "^2.0.2",
"jsdoc-plugin-typescript": "^2.0.1",
"karma": "^4.1.0",
"karma-chrome-launcher": "2.2.0",
@@ -84,7 +95,13 @@
"pixelmatch": "^4.0.2",
"pngjs": "^3.4.0",
"proj4": "2.5.0",
"prop-types": "^15.7.2",
"puppeteer": "~1.16.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-markdown": "^4.0.8",
"react-syntax-highlighter": "^10.2.1",
"react-typography": "^0.16.19",
"rollup": "^1.12.0",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^10.0.0",
@@ -95,6 +112,8 @@
"sinon": "^7.3.2",
"terser-webpack-plugin": "^1.2.3",
"typescript": "^3.4.5",
"typography": "^0.16.19",
"typography-theme-alton": "^0.16.19",
"url-polyfill": "^1.1.5",
"walk": "^2.3.9",
"webpack": "4.31.0",

3
site/.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "tschaub/react"
}

35
site/components/Class.jsx Normal file
View File

@@ -0,0 +1,35 @@
import {object} from 'prop-types';
import React from 'react';
import Markdown from 'react-markdown';
import Code from './Code';
import Parameter from './Parameter';
function Class({cls, module, helper}) {
const exportedName = module.getExportedName(cls.name);
let importCode;
if (exportedName === 'default') {
importCode = `import ${cls.name} from '${module.id}';`;
} else {
importCode = `import {${exportedName}} from '${module.id}';`;
}
return (
<div>
<h3>{cls.name}</h3>
<Code value={importCode} />
<Markdown source={cls.doc.classdesc} renderers={{code: Code}} />
<h6>Parameters</h6>
<ul>
{cls.doc.params && cls.doc.params.map(param => <Parameter param={param} module={module} helper={helper} />)}
</ul>
</div>
);
}
Class.propTypes = {
cls: object.isRequired,
module: object.isRequired,
helper: object.isRequired
};
export default Class;

26
site/components/Code.jsx Normal file
View File

@@ -0,0 +1,26 @@
import React, {PureComponent} from 'react';
import {string} from 'prop-types';
import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter';
import {coy} from 'react-syntax-highlighter/dist/styles/prism';
class Code extends PureComponent {
render() {
let language = this.props.language;
if (!language) {
language = 'js';
}
return (
<SyntaxHighlighter language={language} style={coy}>
{this.props.value}
</SyntaxHighlighter>
);
}
}
Code.propTypes = {
value: string.isRequired,
language: string
};
export default Code;

35
site/components/Func.jsx Normal file
View File

@@ -0,0 +1,35 @@
import {object} from 'prop-types';
import React from 'react';
import Markdown from 'react-markdown';
import Code from './Code';
import Parameter from './Parameter';
function Func({func, module, helper}) {
const exportedName = module.getExportedName(func.name);
let importCode;
if (exportedName === 'default') {
importCode = `import ${func.name} from '${module.id}';`;
} else {
importCode = `import {${exportedName}} from '${module.id}';`;
}
return (
<div>
<h3>{func.name}</h3>
<Code value={importCode} />
<Markdown source={func.doc.description} renderers={{code: Code}} />
<h6>Parameters</h6>
<ul>
{func.doc.params && func.doc.params.map(param => <Parameter param={param} module={module} helper={helper} />)}
</ul>
</div>
);
}
Func.propTypes = {
func: object.isRequired,
module: object.isRequired,
helper: object.isRequired
};
export default Func;

View File

@@ -0,0 +1,26 @@
import {object} from 'prop-types';
import React from 'react';
import Class from './Class';
import Func from './Func';
function Module({module, helper}) {
return (
<div>
<hr />
<h2>{module.id}</h2>
{module.classes.map(cls => (
<Class key={cls.name} cls={cls} module={module} helper={helper} />
))}
{module.functions.map(func => (
<Func key={func.name} func={func} module={module} helper={helper} />
))}
</div>
);
}
Module.propTypes = {
module: object.isRequired,
helper: object.isRequired
};
export default Module;

View File

@@ -0,0 +1,20 @@
import {object} from 'prop-types';
import React from 'react';
import Type from './Type';
function Parameter({param, module, helper}) {
return (
<li>
<code>{param.name}</code> - {param.description} {param.optional && <span>(optional)</span>}<br/>
{param.type.names.map(longName => <Type longName={longName} module={module} helper={helper} />)}
</li>
);
}
Parameter.propTypes = {
param: object.isRequired,
module: object.isRequired,
helper: object.isRequired
};
export default Parameter;

27
site/components/Type.jsx Normal file
View File

@@ -0,0 +1,27 @@
import {object} from 'prop-types';
import React from 'react';
import Parameter from './Parameter';
function Type({longName, module, helper}) {
const type = helper.getTypeDef(longName);
if (!type) {
return <code>{longName}</code>;
}
return (
<div>
<code>{type.doc.type.names}</code>
<ul>
{type.doc.properties && type.doc.properties.map(prop => <Parameter param={prop} module={module} helper={helper} />)}
</ul>
</div>
);
}
Type.propTypes = {
longName: object.isRequired,
module: object.isRequired,
helper: object.isRequired
};
export default Type;

View File

@@ -0,0 +1,8 @@
import styled from '@emotion/styled';
import {baseSpacingPx} from '../util/typography';
export const Page = styled.div({
display: 'flex',
flexDirection: 'column',
margin: 2 * baseSpacingPx
});

26
site/pages/API.js Normal file
View File

@@ -0,0 +1,26 @@
import {object} from 'prop-types';
import React from 'react';
import {Page} from '../components/layout';
import Module from '../components/Module';
import {getHelper} from '../util/api';
function API({pageContext: {docs}}) {
const helper = getHelper(docs);
return (
<Page>
<h1>API</h1>
{helper.modules
.filter(module => module.visible)
.map(module => (
<Module key={module.id} module={module} helper={helper} />
))}
</Page>
);
}
API.propTypes = {
pageContext: object.isRequired
};
export default API;

33
site/pages/Info.js Normal file
View File

@@ -0,0 +1,33 @@
import {object} from 'prop-types';
import React from 'react';
import {Page} from '../components/layout';
function Info({pageContext: {docs}}) {
return (
<Page>
<h1>API</h1>
<table>
<tbody>
<tr>
<th>kind</th>
<th>longname</th>
<th>memberof</th>
</tr>
{docs.map(doc => (
<tr key={doc.longname}>
<td>{doc.kind}</td>
<td>{doc.longname}</td>
<td>{doc.memberof}</td>
</tr>
))}
</tbody>
</table>
</Page>
);
}
Info.propTypes = {
pageContext: object.isRequired
};
export default Info;

205
site/util/api.js Normal file
View File

@@ -0,0 +1,205 @@
class FunctionDoc {
constructor(doc) {
this.name = doc.name;
this.doc = doc;
}
}
class TypedefDoc {
constructor(doc) {
this.name = doc.name;
this.doc = doc;
}
}
class ClassDoc {
constructor(name) {
this.name = name;
}
processDoc(doc) {
if (doc.kind === 'class') {
this.doc = doc;
}
}
}
class ModuleDoc {
constructor(id) {
this.id = id;
this.classLookup = {};
this.classes = [];
this.functionLookup = {};
this.functions = [];
}
processDoc(doc) {
if (doc.kind === 'module') {
this.doc = doc;
//console.log('processing module: ' + doc.longname)
return;
}
if (doc.kind === 'class') {
const name = nameFromLongname(doc.longname);
if (!(name in this.classLookup)) {
const cls = new ClassDoc(name);
this.classLookup[name] = cls;
this.classes.push(cls);
}
this.classLookup[name].processDoc(doc);
return;
}
if (doc.kind === 'function') {
if (nameFromLongname(doc.memberof)) {
// belongs to a class or other
return;
}
if (doc.name in this.functionLookup) {
throw new Error(`Duplicate function ${doc.name} in ${this.id}`);
}
const func = new FunctionDoc(doc);
this.functionLookup[doc.name] = func;
this.functions.push(func);
return;
}
}
finalize() {
this.classes.sort(byName);
this.functions.sort(byName);
this.visible = this.classes.length > 0 || this.functions.length > 0;
}
getExportedName(localName) {
if (!this.doc || !this.doc.exportMap) {
throw new Error(`Expected to find export map in module doc: ${this.id}`);
}
if (!(localName in this.doc.exportMap)) {
throw new Error(
`No local name "${localName}" in export map for module: ${this.id}`
);
}
return this.doc.exportMap[localName];
}
}
const longnameRE = /^module:(?<module>.*?)([~\.](?<name>\w+)(#(?<member>\w+))?(:(?<type>\w+))?)?$/;
function moduleIDFromLongname(longname) {
const match = longname.match(longnameRE);
if (!match) {
throw new Error(`could not match module id in longname: ${longname}`);
}
return match.groups.module;
}
export function nameFromLongname(longname) {
const match = longname.match(longnameRE);
if (!match) {
throw new Error(`could not match name in longname: ${longname}`);
}
return match.groups.name;
}
function memberFromLongname(longname) {
const match = longname.match(longnameRE);
if (!match) {
throw new Error(`could not match member in longname: ${longname}`);
}
return match.groups.member;
}
function byName(a, b) {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
}
function byModuleId(a, b) {
const aParts = a.id.split('/');
const bParts = b.id.split('/');
const len = Math.max(aParts.length, bParts.length);
for (let i = 0; i < len; ++i) {
if (aParts[i] && bParts[i]) {
if (aParts[i] < bParts[i]) {
return -1;
}
if (aParts[i] > bParts[i]) {
return 1;
}
} else if (!aParts[i]) {
return -1;
} else {
return 1;
}
}
return 0;
}
class DocHelper {
constructor(docs) {
this.moduleLookup = {};
this.modules = [];
this.typedefLookup = {};
docs.forEach(doc => {
// typedef are indexed by long name
if (doc.kind === 'typedef') {
if (doc.name in this.typedefLookup) {
throw new Error(`Duplicate type definition ${doc.name} in ${this.id}`);
}
const type = new TypedefDoc(doc);
this.typedefLookup[doc.longname] = type;
return;
}
const moduleID = moduleIDFromLongname(doc.longname);
if (!(moduleID in this.moduleLookup)) {
const module = new ModuleDoc(moduleID);
this.moduleLookup[moduleID] = module;
this.modules.push(module);
}
const module = this.moduleLookup[moduleID];
module.processDoc(doc);
});
this.modules.sort(byModuleId);
this.modules.forEach(module => module.finalize());
}
getTypeDef(longName) {
this.typedefLookup[longName] && console.log(this.typedefLookup[longName]);
return this.typedefLookup[longName];
}
}
let cachedDocs;
let cachedHelper;
export function getHelper(docs) {
if (docs !== cachedDocs) {
if (cachedDocs) {
console.warn('creating new doc helper'); // eslint-disable-line
}
cachedHelper = new DocHelper(docs);
cachedDocs = docs;
}
return cachedHelper;
}

9
site/util/typography.js Normal file
View File

@@ -0,0 +1,9 @@
import Typography from 'typography';
import theme from 'typography-theme-alton';
const typography = new Typography(theme);
export const baseSpacingPx = parseInt(theme.baseFontSize, 10);
export {theme};
export default typography;