diff options
Diffstat (limited to 'platform/darwin/scripts/update-examples.js')
-rw-r--r-- | platform/darwin/scripts/update-examples.js | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/platform/darwin/scripts/update-examples.js b/platform/darwin/scripts/update-examples.js new file mode 100644 index 0000000000..6291326068 --- /dev/null +++ b/platform/darwin/scripts/update-examples.js @@ -0,0 +1,152 @@ +'use strict'; + +const fs = require('fs'); +const execFileSync = require('child_process').execFileSync; +const _ = require('lodash'); + +const examplesSrc = fs.readFileSync('platform/darwin/test/MGLDocumentationExampleTests.swift', 'utf8'); + +// Regex extracts the following block +// /** Front matter to describe the example. **/ +// func testMGLClass$member() { +// ... +// //#-example-code +// let sampleCode: String? +// //#-end-example-code +// ... +// } +// +// into the following regex groups: +// 1 (test method name): "MGLClass" or "MGLClass$member" or "MGLClass$initWithArg_anotherArg_" +// 2 (indentation): " " +// 3 (sample code): "let sampleCode: String?" +const exampleRegex = /func test([\w$]+)\s*\(\)\s*\{[^]*?\n([ \t]+)\/\/#-example-code\n([^]+?)\n\2\/\/#-end-example-code\n/gm; + +/** + * Returns the given source with example code inserted into the documentation + * comment for the symbol at the given one-based line. + * + * @param {String} src Source code to insert the example code into. + * @param {Number} line One-based line number of the symbol being documented. + * @param {String} exampleCode Example code to insert. + * @returns {String} `src` with `exampleCode` inserted just above `line`. + */ +function completeSymbolInSource(src, line, exampleCode) { + // Split the file contents right before the symbol declaration (but after its documentation comment). + let srcUpToSymbol = src.split('\n', line - 1).join('\n'); + let srcFromSymbol = src.substr(srcUpToSymbol.length); + + // Match the documentation comment block that is not followed by the beginning or end of a declaration. + let commentMatch = srcUpToSymbol.match(/\/\*\*\s*(?:[^*]|\*(?!\/))+?\s*\*\/[^;{}]*?$/); + + // Replace the Swift code block with the test method’s contents. + let completedComment = commentMatch[0].replace(/^([ \t]*)```swift\n[^]*?```/m, function (m, indentation) { + // Apply the original indentation to each line. + return ('```swift\n' + exampleCode + '\n```').replace(/^/gm, indentation); + }); + + // Splice the modified comment into the overall file contents. + srcUpToSymbol = (srcUpToSymbol.substr(0, commentMatch.index) + completedComment + + srcUpToSymbol.substr(commentMatch.index + commentMatch[0].length)); + return srcUpToSymbol + srcFromSymbol; +} + +let examples = {}; +let match; +while ((match = exampleRegex.exec(examplesSrc)) !== null) { + let testMethodName = match[1], + indentation = match[2], + exampleCode = match[3]; + + // Trim leading whitespace from the example code. + exampleCode = exampleCode.replace(new RegExp('^' + indentation, 'gm'), ''); + + examples[testMethodName] = exampleCode; +} + +function completeExamples(os) { + console.log(`Installing ${os} SDK examples…`); + + let sdk = os === 'iOS' ? 'iphonesimulator' : 'macosx'; + let sysroot = execFileSync('xcrun', ['--show-sdk-path', '--sdk', sdk]).toString().trim(); + let umbrellaPath = `platform/${os.toLowerCase()}/src/Mapbox.h`; + let docArgs = ['doc', '--objc', umbrellaPath, '--', + '-x', 'objective-c', '-I', 'platform/darwin/src/', '-isysroot', sysroot]; + let docStr = execFileSync('sourcekitten', docArgs).toString().trim(); + let docJson = JSON.parse(docStr); + _.forEach(docJson, function (result) { + _.forEach(result, function (structure, path) { + let src; + let newSrc; + // Recursively search for code blocks in documentation comments and populate + // them with example code from the test methods. Find and replace the code + // blocks in reverse to keep the SourceKitten line numbers accurate. + _.forEachRight(structure['key.substructure'], function completeSubstructure(substructure, idx, substructures, symbolPath) { + if (!symbolPath) { + symbolPath = [substructure['key.name']]; + } + _.forEachRight(substructure['key.substructure'], function (substructure, idx, substructures) { + completeSubstructure(substructure, idx, substructures, _.concat(symbolPath, substructure['key.name'])); + }); + + let comment = substructure['key.doc.comment']; + if (!comment || !comment.match(/^(?:\s*)```swift\n/m)) { + return; + } + + // Lazily read in the existing file. + if (!src) { + newSrc = src = fs.readFileSync(path, 'utf8'); + } + + // Get the contents of the test method whose name matches the symbol path. + let testMethodName = symbolPath.join('$').replace(/\$[+-]/, '$').replace(/:/g, '_'); + let example = examples[testMethodName]; + if (!example) { + console.error(`MGLDocumentationExampleTests.${testMethodName}() not found.`); + process.exit(1); + } + + // Resolve conditional compilation blocks. + example = example.replace(/^(\s*)#if\s+os\((iOS|macOS)\)\n([^]*?)(?:^\1#else\n([^]*?))?^\1#endif\n/gm, + function (m, indentation, ifOs, ifCase, elseCase) { + return (os === ifOs ? ifCase : elseCase).replace(new RegExp('^ ', 'gm'), ''); + }); + + // Insert the test method contents into the documentation comment just + // above the substructure. + let startLine = substructure['key.parsed_scope.start']; + newSrc = completeSymbolInSource(newSrc, startLine, example); + }); + + if (!src) { + return; + } + + // Write out the modified file contents. + if (src === newSrc) { + console.log('Skipping', path); + } else { + console.log('Updating', path); + if (['0', 'false'].indexOf(process.env.DRY_RUN || '0') !== -1) { + fs.writeFileSync(path, newSrc); + } + } + }); + }); +} + +function ensureSourceKittenIsInstalled() { + try { + execFileSync('which', ['sourcekitten']); + } catch (e) { + console.log(`Installing SourceKitten via Homebrew…`); + execFileSync('brew', ['install', 'sourcekitten']); + } +} + +ensureSourceKittenIsInstalled(); + +// Where a particular comment is part of both SDKs, prefer the iOS example. +completeExamples('macOS'); +completeExamples('iOS'); |