diff --git a/.gitignore b/.gitignore index 5e2d75c9807..4256959b023 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /docs/components/api.json /dist/ /docs/pages/dist/ +/docs/img/dist/ *.js.map node_modules package-lock.json diff --git a/batfish.config.js b/batfish.config.js index f287119eedf..3567c568d38 100644 --- a/batfish.config.js +++ b/batfish.config.js @@ -1,4 +1,5 @@ -const { webpack } = require('@mapbox/batfish'); +const webpack = require('webpack'); +const mapboxAssembly = require('@mapbox/mbx-assembly'); const path = require('path'); module.exports = () => { @@ -7,9 +8,13 @@ module.exports = () => { siteOrigin: 'https://docs.mapbox.com', pagesDirectory: `${__dirname}/docs/pages`, outputDirectory: path.join(__dirname, '_site'), + browserslist: mapboxAssembly.browsersList, + postcssPlugins: mapboxAssembly.postcssPipeline.plugins, stylesheets: [ + require.resolve('@mapbox/mbx-assembly/dist/assembly.css'), + require.resolve('@mapbox/dr-ui/css/docs-prose.css'), `${__dirname}/docs/components/site.css`, - `${__dirname}/docs/components/prism_highlight.css`, + require.resolve('@mapbox/dr-ui/css/prism.css'), `${__dirname}/vendor/docs-page-shell/page-shell-styles.css` ], applicationWrapperPath: `${__dirname}/docs/components/application-wrapper.js`, @@ -33,14 +38,40 @@ module.exports = () => { filename: `${__dirname}/vendor/docs-page-shell/page-shell-script.js` } ], + jsxtremeMarkdownOptions: { + wrapper: path.join(__dirname, './docs/components/markdown-page-shell.js'), + rehypePlugins: [ + require('@mapbox/dr-ui/plugins/add-links-to-headings'), + require('@mapbox/dr-ui/plugins/make-table-scroll') + ] + }, dataSelectors: { examples: ({pages}) => { return pages .filter(({path, frontMatter}) => /\/example\//.test(path) && frontMatter.tags) - .map(({frontMatter}) => frontMatter); + .map(example => { + return { + path: example.path, + title: example.frontMatter.title, + description: example.frontMatter.description, + tags: example.frontMatter.tags, + pathname: example.frontMatter.pathname + }; + }); + }, + listSubfolders: data => { + const folders = data.pages + .filter(file => { + return file.path.split('/').length === 4; + }) + .map(folder => { + return folder; + }); + return folders; } }, - devBrowserslist: false + devBrowserslist: false, + babelInclude: ['documentation'] }; // Local builds treat the `dist` directory as static assets, allowing you to test examples against the diff --git a/docs/README.md b/docs/README.md index 23be2ec1ad1..ea6d3fc3eef 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,7 +23,7 @@ code for the example, and a `.js` file containing example boilerplate and front * `title`: A short title for the example in **sentence case** as a **verb phrase** * `description`: A one sentence description of the example -* `tags`: An array of tags for the example, which determine the sections it is listed in in the sidebar navigation +* `tags`: An array of tags for the example, which determine the sections it is listed in in the sidebar navigation, see `docs/data/tags.js` for a list of tags * `pathname`: The relative path of the example, including leading `/mapbox-gl-js/example/` path In the `.html` file, write the HTML and JavaScript constituting the example. @@ -33,6 +33,13 @@ In the `.html` file, write the HTML and JavaScript constituting the example. * Do **not** use custom styles from your personal account. Use only the default `mapbox` account styles. * When embedding literal JSON (GeoJSON or Mapbox style snippets) into script code, double-quote property names and string values. Elsewhere, use single-quoted strings. +Every example **must** have an accompanying image: + +1. Run `npm run create-image `. The script will take a screenshot of the map in the example and save it to `docs/img/src/`. Commit the image. +2. Run `npm run start-docs` to verify that your example image is loading as expected. + +💡 If `npm run create-image` does not generate an ideal image. You can also take a screenshot of it yourself by running the site locally with `npm run start-docs` and taking a screenshot of the example map in PNG format. Resize it to 1200 x 500 pixels and save it in the `docs/img/src` folder. + ## Running the Documentation Server Locally To start a documentation server locally run @@ -42,6 +49,8 @@ npm run start-docs The command will print the URL you can use to view the documentation. +💡 If you receive an error related to `@mapbox/appropriate-images`, try `nvm use 8 && npm run start-docs`. + ## Committing and Publishing Documentation The mapbox-gl-js repository has both `master` and `publisher-production` as active branches. The **`master` branch** is used for mainline code development: the next version of mapbox-gl-js will come from the code in this branch, and it may contain documentation and examples for APIs that are not yet part of a public release. The **`publisher-production` branch** is published to https://www.mapbox.com/mapbox-gl-js/ on any push to the branch. For the purposes of documentation changes, use these two branches as follows: diff --git a/docs/bin/appropriate-images.js b/docs/bin/appropriate-images.js new file mode 100644 index 00000000000..e3561cefe3a --- /dev/null +++ b/docs/bin/appropriate-images.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node +'use strict'; + +const path = require('path'); // eslint-disable-line import/no-commonjs +const appropriateImages = require('@mapbox/appropriate-images'); // eslint-disable-line import/no-commonjs +const imageConfig = require('../img/dist/image.config.json'); // eslint-disable-line import/no-commonjs + +appropriateImages.createCli(imageConfig, { + inputDirectory: path.join(__dirname, '../img/src'), + outputDirectory: path.join(__dirname, '../img/dist') +}); diff --git a/docs/bin/build-image-config.js b/docs/bin/build-image-config.js new file mode 100644 index 00000000000..f0b96caa2a5 --- /dev/null +++ b/docs/bin/build-image-config.js @@ -0,0 +1,24 @@ +#!/usr/bin/env node +// builds image.config.json +// this configuration file is required to generate the appropriate images sizes with docs/bin/appropriate-images.js +// it is also required in react component that loads the image in components/appropriate-image.js +const imagePath = './docs/img/src/'; + +const imageConfig = require('fs').readdirSync(imagePath).reduce((obj, image) => { + const ext = require('path').extname(`${imagePath}${image}`); + // only process png + if (ext === '.png') { + const key = image.replace(ext, ''); + // set sizes for all images + const sizes = [{ width: 800 }, { width: 500 }]; + // set additional sizes for specific images + if (key === 'simple-map') sizes.push({ width: 1200 }); + obj[key] = { + basename: image, + sizes + }; + } + return obj; +}, {}); + +require('fs').writeFileSync('./docs/img/dist/image.config.json', JSON.stringify(imageConfig)); diff --git a/docs/bin/create-image.js b/docs/bin/create-image.js new file mode 100644 index 00000000000..d3e75c5f399 --- /dev/null +++ b/docs/bin/create-image.js @@ -0,0 +1,63 @@ +'use strict'; + +const puppeteer = require('puppeteer'); // eslint-disable-line +const path = require('path'); // eslint-disable-line +const pack = require('../../package.json'); // eslint-disable-line + +const fileName = process.argv[2]; +const token = process.argv[3] || process.env.MAPBOX_ACCESS_TOKEN || process.env.MapboxAccessToken; + +if (!token || !fileName) { + throw new Error('\n Usage: npm run create-image \nExample: npm run create-image 3d-buildings pk000011110000111100001111\n\n'); +} + +// strip file extension from file name +const fileNameFormatted = fileName.replace('.html', '').replace('.js', ''); +// get the example contents/snippet +const snippet = require('fs').readFileSync(path.resolve(__dirname, '..', 'pages', 'example', `${fileNameFormatted}.html`), 'utf-8'); +// create an HTML page to display the example snippet +const html = ` + + + + + + + + + +${snippet} + +`; + +// initilize puppeteer +(async() => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + // set html for page and then wait until mapbox-gl-js loads + await page.setContent(html, {waitUntil: 'networkidle2'}); + // set viewport and double deviceScaleFactor to get a closer shot of the map + await page.setViewport({ + width: 600, + height: 600, + deviceScaleFactor: 2 + }); + // create screenshot + await page.screenshot({ + path: `./docs/img/src/${fileNameFormatted}.png`, + type: 'png', + clip: { + x: 0, + y: 0, + width: 600, + height: 250 + } + }).then(() => console.log(`Created ./docs/img/src/${fileNameFormatted}.png`)) + .catch((err) => { console.log(err); }); + await browser.close(); +})(); diff --git a/docs/components/api-item-member.js b/docs/components/api-item-member.js new file mode 100644 index 00000000000..173c8e3006e --- /dev/null +++ b/docs/components/api-item-member.js @@ -0,0 +1,76 @@ +import React from 'react'; +import GithubSlugger from 'github-slugger'; +import createFormatters from 'documentation/src/output/util/formatters'; +import LinkerStack from 'documentation/src/output/util/linker_stack'; +import docs from '../components/api.json'; // eslint-disable-line import/no-unresolved +import ApiItem from '../components/api-item'; +import Icon from '@mapbox/mr-ui/icon'; + +const linkerStack = new LinkerStack({}) + .namespaceResolver(docs, (namespace) => { + const slugger = new GithubSlugger(); + return `#${slugger.slug(namespace)}`; + }); + +const formatters = createFormatters(linkerStack.link); + +class ApiItemMember extends React.Component { + constructor(props) { + super(props); + this.state = {disclosed: false}; + this.hashChange = this.hashChange.bind(this); + } + + href = m => `#${m.namespace.toLowerCase()}` + + render() { + const member = this.props; + return ( +
+
+
+ +
+ + {this.state.disclosed && +
+ +
} +
+ ); + } + + hashChange() { + if (window.location.hash === this.href(this.props)) { + this.setState({disclosed: true }); + } + } + + componentDidMount() { + window.addEventListener("hashchange", this.hashChange); + this.hashChange(); + } + + componentWillUnmount() { + window.removeEventListener("hashchange", this.hashChange); + } +} + +export default ApiItemMember; diff --git a/docs/components/api-item.js b/docs/components/api-item.js new file mode 100644 index 00000000000..33f0ac78b15 --- /dev/null +++ b/docs/components/api-item.js @@ -0,0 +1,169 @@ +import React from 'react'; +import createFormatters from 'documentation/src/output/util/formatters'; +import LinkerStack from 'documentation/src/output/util/linker_stack'; +import GithubSlugger from 'github-slugger'; +import {highlightJavascript} from '../components/prism_highlight.js'; +import docs from '../components/api.json'; // eslint-disable-line import/no-unresolved +import ApiItemMember from './api-item-member'; +import IconText from '@mapbox/mr-ui/icon-text'; + + +const linkerStack = new LinkerStack({}) + .namespaceResolver(docs, (namespace) => { + const slugger = new GithubSlugger(); + return `#${slugger.slug(namespace)}`; + }); + +const formatters = createFormatters(linkerStack.link); + +class ApiItem extends React.Component { + + md = (ast, inline) => { + if (inline && ast && ast.children.length && ast.children[0].type === 'paragraph') { + ast = { + type: 'root', + children: ast.children[0].children.concat(ast.children.slice(1)) + }; + } + return ; + } + formatType = type => + + render() { + const section = this.props; + + const empty = members => !members || members.length === 0; + + const membersList = (members, title) => !empty(members) && +
+
{title}
+
+ {members.map((member, i) => )} +
+
; + + return ( +
+ {!this.props.nested && +
+

{section.name}

+ {section.context && section.context.github && + {section.context.github.path}} +
} + + {this.md(section.description)} + + {!empty(section.augments) && +

Extends {section.augments.map((tag, i) => )}.

} + + {section.kind === 'class' && + !section.interface && + (!section.constructorComment || section.constructorComment.access !== 'private') && +
} + + {section.version &&
Version: {section.version}
} + {section.license &&
License: {section.license}
} + {section.author &&
Author: {section.author}
} + {section.copyright &&
Copyright: {section.copyright}
} + {section.since &&
Since: {section.since}
} + + {!empty(section.params) && (section.kind !== 'class' || !section.constructorComment || section.constructorComment.access !== 'private') && +
+
Parameters
+
+ {section.params.map((param, i) =>
+
+ {param.name} + ({this.formatType(param.type)}) + {param.default && {'('}default {param.default}{')'}} + {this.md(param.description, true)} +
+ {param.properties && +
+ + + + + + + + + + + + + {param.properties.map((property, i) => + + + )} + +
NameDescription
+ {property.name}
+ {this.formatType(property.type)}
+ {property.default && default {property.default}} +
{this.md(property.description, true)}
+
} +
)} +
+
} + + {!empty(section.properties) && +
+
Properties
+
+ {section.properties.map((property, i) =>
+ {property.name} + ({this.formatType(property.type)}) + {property.default && {'('}default {property.default}{')'}} + {property.description && : {this.md(property.description, true)}} + {property.properties && +
    + {property.properties.map((property, i) =>
  • + {property.name} {this.formatType(property.type)} + {property.default && {'('}default {property.default}{')'}} + {this.md(property.description)} +
  • )} +
} +
)} +
+
} + + {section.returns && section.returns.map((ret, i) =>
+
Returns
+ {this.formatType(ret.type)} + {ret.description && : {this.md(ret.description, true)}} +
)} + + {!empty(section.throws) && +
+
Throws
+
    + {section.throws.map((throws, i) =>
  • {this.formatType(throws.type)}: {this.md(throws.description, true)}
  • )} +
+
} + + {!empty(section.examples) && +
+
Example
+ {section.examples.map((example, i) =>
+ {example.caption &&

{this.md(example.caption)}

} + {highlightJavascript(example.description)} +
)} +
} + + {membersList(section.members.static, 'Static Members')} + {membersList(section.members.instance, 'Instance Members')} + {membersList(section.members.events, 'Events')} + + {!empty(section.sees) && +
+
Related
+
    {section.sees.map((see, i) =>
  • {this.md(see, true)}
  • )}
+
} +
+ ); + } +} + +export default ApiItem; diff --git a/docs/components/api-navigation.js b/docs/components/api-navigation.js new file mode 100644 index 00000000000..3ad9f24d367 --- /dev/null +++ b/docs/components/api-navigation.js @@ -0,0 +1,76 @@ +import React from 'react'; +import docs from '../components/api.json'; // eslint-disable-line import/no-unresolved +import Icon from '@mapbox/mr-ui/icon'; + +function href(m) { + return `#${m.namespace.toLowerCase()}`; +} + +class TableOfContentsNote extends React.Component { + render() { + const doc = this.props; + return ( +
  • + {doc.name} +
  • + ); + } +} + +class TableOfContentsItem extends React.Component { + constructor(props) { + super(props); + this.state = {disclosed: false}; + } + + render() { + const doc = this.props; + + const empty = members => !members || members.length === 0; + if (empty(doc.members.static) && empty(doc.members.instance) && empty(doc.members.events)) { + return
  • {doc.name}
  • ; + } + + const membersList = (members, title, sigil) => members && members.length !== 0 && + ; + + return ( +
  • + this.setState({disclosed: !this.state.disclosed})}> + {doc.name} + + + + {this.state.disclosed && +
    + {membersList(doc.members.static, 'Static members', '.')} + {membersList(doc.members.instance, 'Instance members', '#')} + {membersList(doc.members.events, 'Events', 'â“” ')} +
    } +
  • + ); + } +} + +class ApiNavigation extends React.Component { + render() { + return ( +
    +
      + {docs.map((doc, i) => (doc.kind === 'note') ? + : + )} +
    +
    + ); + } +} + +export default ApiNavigation; diff --git a/docs/components/application-wrapper.js b/docs/components/application-wrapper.js index 0c8b07bf89e..b5eb173ea8e 100644 --- a/docs/components/application-wrapper.js +++ b/docs/components/application-wrapper.js @@ -2,6 +2,7 @@ import React from 'react'; if (typeof window !== 'undefined') { window.MapboxPageShellProduction = true; + import(/* webpackChunkName: "assembly-js" */ '@mapbox/mbx-assembly/dist/assembly.js'); } class ApplicationWrapper extends React.Component { diff --git a/docs/components/appropriate-image.js b/docs/components/appropriate-image.js new file mode 100644 index 00000000000..2805bc305c2 --- /dev/null +++ b/docs/components/appropriate-image.js @@ -0,0 +1,12 @@ +import { scopeAppropriateImage } from '@mapbox/appropriate-images-react'; +import imageConfig from '../img/dist/image.config.json'; // eslint-disable-line import/no-unresolved +// image.config.json is generated on build + +// See https://github.com/mapbox/appropriate-images-react#appropriateimage +// The required prop is `imageId`, which must correspond to a key in the +// imageConfig. +const AppropriateImage = scopeAppropriateImage(imageConfig, { + transformUrl: url => require(`../img/dist/${url}`) +}); + +export default AppropriateImage; diff --git a/docs/components/copyable.js b/docs/components/copyable.js index 85390210b27..5e61b0a96e0 100644 --- a/docs/components/copyable.js +++ b/docs/components/copyable.js @@ -1,27 +1,21 @@ import React from 'react'; -import {copy} from 'execcommand-copy'; +import CodeSnippet from '@mapbox/mr-ui/code-snippet'; +import Prism from 'prismjs'; +const highlightTheme = require('raw-loader!@mapbox/dr-ui/css/prism.css'); // eslint-disable-line import/no-commonjs + export default class extends React.Component { - constructor(props) { - super(props); - this.state = {copied: false}; - } render() { return ( -
    - this.copy(e)}>{this.state.copied && 'Copied to clipboard!'} -
    { this.ref = ref; }}>{this.props.children}
    +
    + { analytics.track('Copied example with clipboard'); }} + highlightedCode={Prism.highlight(this.props.children, Prism.languages[this.props.lang])} + highlightThemeCss={highlightTheme} + />
    ); } - - copy(e) { - e.preventDefault(); - copy(this.ref.innerText); - analytics.track('Copied example with clipboard'); - this.setState({copied: true}); - setTimeout(() => this.setState({copied: false}), 1000); - } } diff --git a/docs/components/example.js b/docs/components/example.js index 9d023090b76..daaa0da4a90 100644 --- a/docs/components/example.js +++ b/docs/components/example.js @@ -1,27 +1,13 @@ import React from 'react'; -import {prefixUrl} from '@mapbox/batfish/modules/prefix-url'; import urls from './urls'; import md from './md'; import PageShell from './page_shell'; -import LeftNav from './left_nav'; -import TopNav from './top_nav'; -import {highlightMarkup} from './prism_highlight'; +import Prism from 'prismjs'; import supported from '@mapbox/mapbox-gl-supported'; -import {copy} from 'execcommand-copy'; -import examples from '@mapbox/batfish/data/examples'; // eslint-disable-line import/no-unresolved -import entries from 'object.entries'; +import Icon from '@mapbox/mr-ui/icon'; +import CodeSnippet from '@mapbox/mr-ui/code-snippet'; -const tags = { - "styles": "Styles", - "layers": "Layers", - "sources": "Sources", - "user-interaction": "User interaction", - "camera": "Camera", - "controls-and-overlays": "Controls and overlays", - "geocoder": "Geocoder", - "browser-support": "Browser support", - "internationalization": "Internationalization support" -}; +const highlightTheme = require('raw-loader!@mapbox/dr-ui/css/prism.css'); // eslint-disable-line import/no-commonjs export default function (html) { return class extends React.Component { @@ -29,7 +15,6 @@ export default function (html) { super(props); this.state = { filter: '', - copied: false, token: undefined }; } @@ -81,58 +66,35 @@ ${html} render() { const {frontMatter} = this.props; - const filter = this.state.filter.toLowerCase().trim(); return ( - -
    - this.setState({filter: e.target.value})} - type='text' className='space-bottom' name='filter' placeholder='Filter examples' /> -
    - {entries(tags).map(([tag, title], i) => -
    - {!filter &&

    {title}

    } - {examples - .filter(({tags, title, description}) => - tags.indexOf(tag) !== -1 && (title.toLowerCase().indexOf(filter) !== -1 || description.toLowerCase().indexOf(filter) !== -1)) - .map(({pathname, title}, i) => - {title} - )} +
    +
    +
    +

    {frontMatter.title}

    +
    {md(frontMatter.description)}
    + + {!supported() && +
    +
    +
    Mapbox GL unsupported
    +
    Mapbox GL requires WebGL support. Please check that you are using a supported browser and that WebGL is enabled.
    +
    +
    }
    - )} - - -
    - - -
    -
    - - -
    -
    {frontMatter.title}
    {md(frontMatter.description)}
    - {!supported() && -
    -
    -
    Mapbox GL unsupported
    -
    Mapbox GL requires WebGL support. Please check that you are using a supported browser and that WebGL is enabled.
    -
    -
    } -
    - - {supported() && -