summaryrefslogtreecommitdiff
path: root/platform/darwin/scripts/update-examples.js
diff options
context:
space:
mode:
Diffstat (limited to 'platform/darwin/scripts/update-examples.js')
-rw-r--r--platform/darwin/scripts/update-examples.js152
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');