From 3a5d6a114bd90a2062701f33c912963334fa2d2b Mon Sep 17 00:00:00 2001
From: Mike Ralphson <mike.ralphson@gmail.com>
Date: Mon, 19 Jul 2021 15:35:54 +0100
Subject: [PATCH 1/2] perf: switch to tempura for templating

---
 .eslintrc.json                   |  10 ++-
 src/_includes/card.html          |  59 ++++++-------
 src/_layouts/apis-page.liquid    |   2 -
 src/assets/javascript/apis.js    |  13 ++-
 src/assets/javascript/main.js    |   2 -
 src/assets/javascript/tempura.js | 138 +++++++++++++++++++++++++++++++
 src/index.md                     |  10 ++-
 7 files changed, 186 insertions(+), 48 deletions(-)
 create mode 100644 src/assets/javascript/tempura.js

diff --git a/.eslintrc.json b/.eslintrc.json
index f588728..f246ece 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -6,7 +6,8 @@
         "jquery": true
     },
     "parserOptions": {
-        "ecmaVersion": 2017
+        "ecmaVersion": 2017,
+        "sourceType": "module"
     },
     "extends": "eslint:recommended",
     "rules": {
@@ -37,7 +38,7 @@
         "callback-return": "off",
         "camelcase": "off",
         "class-methods-use-this": "error",
-        "comma-dangle": "error",
+        "comma-dangle": "warn",
         "comma-spacing": "off",
         "comma-style": [
             "error",
@@ -131,6 +132,7 @@
         "no-extra-parens": "off",
         "no-floating-decimal": "error",
         "no-global-assign": "error",
+        "no-cond-assign": "warn",
         "no-implicit-globals": "error",
         "no-implied-eval": "error",
         "no-inline-comments": "off",
@@ -151,7 +153,7 @@
         "no-multi-spaces": "off",
         "no-multi-str": "error",
         "no-multiple-empty-lines": "error",
-        "no-negated-condition": "error",
+        "no-negated-condition": "warn",
         "no-nested-ternary": "off",
         "no-new": "error",
         "no-new-func": "error",
@@ -252,7 +254,7 @@
         "spaced-comment": "off",
         "strict": "error",
         "symbol-description": "error",
-        "template-curly-spacing": "error",
+        "template-curly-spacing": "warn",
         "unicode-bom": [
             "error",
             "never"
diff --git a/src/_includes/card.html b/src/_includes/card.html
index f2dcbd8..793c7c2 100644
--- a/src/_includes/card.html
+++ b/src/_includes/card.html
@@ -1,56 +1,57 @@
-<script type="text/dot-template">
+<script type="text/tempura">
     {% raw %}
+    {{#expect it}}
     <div class="card">
-        {{? it.classes}}
-          <span class="{{=it.classes}}" title="{{=it.flashTitle}}"><strong>{{=it.flashText}}</strong></span>
-        {{??}}
+        {{#if it.classes}}
+          <span class="{{{it.classes}}}" title="{{{it.flashTitle}}}"><strong>{{{it.flashText}}}</strong></span>
+        {{#else}}
           <span class="spacer"></span>
-        {{?}}
+        {{/if}}
         <header>
-            <h2 title="{{=it.info.title }}">
-              {{? it.externalUrl }}
-                <a href="{{=it.externalUrl }}" target="_blank">{{=it.info.title }}</a>
-              {{??}}
-                {{=it.info.title }}
-              {{?}}
+            <h2 title="{{{it.info.title}}}">
+              {{#if it.externalUrl }}
+                <a href="{{{it.externalUrl}}}" target="_blank">{{it.info.title}}</a>
+              {{#else}}
+                {{it.info.title}}
+              {{/if}}
             </h2>
         </header>
         <section class="api-body">
-            <img src="{{=it.logo.url || 'assets/images/no-logo.svg'}}" alt="{{=it.info.title }} API logo" style="background-color: {{=it.logo.backgroundColor || 'transparent'}}" class="api-logo">
-            <p>{{=it.cardDescription}}</p>
+            <img src="{{it.logo.url || 'assets/images/no-logo.svg'}}" alt="{{it.info.title }} API logo" style="background-color: {{it.logo.backgroundColor || 'transparent'}}" class="api-logo">
+            <p>{{it.cardDescription}}</p>
         </section>
         <footer>
           <h3> OpenAPI: </h3>
-          <h4>Preferred Version - {{=it.preferred}}</h4>
+          <h4>Preferred Version - {{it.preferred}}</h4>
           <ul class="preferred-api">
-            <li><a href="{{=it.api.swaggerUrl}}" target="_blank" >JSON</a></li>
-            <li><a href='{{=it.api.swaggerYamlUrl}}' target="_blank" >YAML</a></li>
-            <li><a href="{{=it.origUrl}}" target='_blank'>Orig</a></li>
-            <li><a href='https://redocly.github.io/redoc/?url={{=it.api.swaggerUrl}}' target="_blank" >Docs</a></li>
+            <li><a href="{{it.api.swaggerUrl}}" target="_blank" >JSON</a></li>
+            <li><a href='{{it.api.swaggerYamlUrl}}' target="_blank" >YAML</a></li>
+            <li><a href="{{it.origUrl}}" target='_blank'>Orig</a></li>
+            <li><a href='https://redocly.github.io/redoc/?url={{it.api.swaggerUrl}}' target="_blank" >Docs</a></li>
           </ul>
-          {{? it.versions }}
+          {{#if it.versions }}
           <details>
             <summary><h4>All Versions</h4></summary>
             <ul class="other-versions">
-              {{~it.versions :version:index}}
+              {{#each it.versions as version}}
               <li>
-                <span>{{=version.version}}</span>
+                <span>{{version.version}}</span>
                 <ul>
-                  <li><a href="{{=version.swaggerUrl}}" target="_blank" >JSON</a></li>
-                  <li><a href="{{=version.swaggerYamlUrl}}" target='_blank'>YAML</a></li>
-                  <li><a href='https://redocly.github.io/redoc/?url={{=version.swaggerUrl}}' target="_blank" >Docs</a></li>
+                  <li><a href="{{version.swaggerUrl}}" target="_blank" >JSON</a></li>
+                  <li><a href="{{version.swaggerYamlUrl}}" target='_blank'>YAML</a></li>
+                  <li><a href='https://redocly.github.io/redoc/?url={{version.swaggerUrl}}' target="_blank" >Docs</a></li>
                 </ul>
               </li>
-              {{~}}
+              {{/each}}
             </ul>
           </details>
-          {{?}}
+          {{/if}}
           <details>
             <summary><h4>Tools</h4></summary>
             <ul class="tools">
-              {{~it.integrations :integration:index}}
-              <li><a href="{{=integration.template}}" target="_blank" >{{=integration.text}}</a></li>
-              {{~}}
+              {{#each it.integrations as integration}}
+              <li><a href="{{integration.template}}" target="_blank" >{{integration.text}}</a></li>
+              {{/each}}
             </ul>
           </details>
         </footer>
diff --git a/src/_layouts/apis-page.liquid b/src/_layouts/apis-page.liquid
index 9a4dcef..41fdec3 100644
--- a/src/_layouts/apis-page.liquid
+++ b/src/_layouts/apis-page.liquid
@@ -18,10 +18,8 @@
   <!-- JS -->
   <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.7.0/marked.js"></script>
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/dot/1.0.3/doT.min.js"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/slideout/0.1.12/slideout.js"></script>
   <!-- inject:js -->
-  <script src="/assets/javascript/apis.js"></script>
   <script src="/assets/javascript/main.js"></script>
   <!-- endinject -->
 
diff --git a/src/assets/javascript/apis.js b/src/assets/javascript/apis.js
index f969df5..9dab00f 100644
--- a/src/assets/javascript/apis.js
+++ b/src/assets/javascript/apis.js
@@ -1,4 +1,4 @@
-'use strict';
+import * as tempura from "./tempura.js";
 
 const dummy = {
   loading: {
@@ -128,16 +128,15 @@ CardModel.prototype.fromAPIs = function(name, apis) {
     return this;
 };
 
-if (window.$) {
-  $(document).ready(function () {
-    var cardTemplateSrc = document.querySelector('script[type="text/dot-template"]').innerText;
-    var cardTemplate = window.doT.compile(cardTemplateSrc);
+export function loadAPIs() {
+    var cardTemplateSrc = document.querySelector('script[type="text/tempura"]').innerText;
+    var cardTemplate = tempura.compile(cardTemplateSrc);
 
     var updateCards = function(data) {
         var fragment = $(document.createDocumentFragment());
         $.each(data, function (name, apis) {
             var model = new CardModel().fromAPIs(name, apis);
-            var view = cardTemplate(model);
+            var view = cardTemplate({it:model});
             fragment.append($(view));
         });
 
@@ -232,5 +231,5 @@ if (window.$) {
         $('#search-input').focus();
     });
 
-  });
 }
+
diff --git a/src/assets/javascript/main.js b/src/assets/javascript/main.js
index a53e87a..ab5d4d7 100644
--- a/src/assets/javascript/main.js
+++ b/src/assets/javascript/main.js
@@ -1,5 +1,3 @@
-'use strict';
-
 function domReady(cb) {
   document.addEventListener("DOMContentLoaded", cb, false);
 }
diff --git a/src/assets/javascript/tempura.js b/src/assets/javascript/tempura.js
new file mode 100644
index 0000000..93dcd79
--- /dev/null
+++ b/src/assets/javascript/tempura.js
@@ -0,0 +1,138 @@
+const ESCAPE = /[&"<]/g, CHARS = {
+	'"': '&quot;',
+	'&': '&amp;',
+	'<': '&lt',
+};
+
+const ENDLINES = /[\r\n]+$/g;
+const CURLY = /{{{?\s*([\s\S]*?)\s*}}}?/g;
+const ARGS = /([a-zA-Z$_][^\s=]*)\s*=\s*((["`'])(?:(?=(\\?))\4.)*?\3|{[^}]*}|\[[^\]]*]|\S+)/g;
+
+// $$1 = escape()
+// $$2 = extra blocks
+// $$3 = template values
+function gen(input, options) {
+	options = options || {};
+
+	let char, num, action, tmp;
+	let last = CURLY.lastIndex = 0;
+	let wip='', txt='', match, inner;
+
+	let extra=options.blocks||{}, stack=[];
+	let initials = new Set(options.props||[]);
+
+	function close() {
+		if (wip.length > 0) {
+			txt += (txt ? 'x+=' : '=') + '`' + wip + '`;';
+		} else if (txt.length === 0) {
+			txt = '="";'
+		}
+		wip = '';
+	}
+
+	while (match = CURLY.exec(input)) {
+		wip += input.substring(last, match.index).replace(ENDLINES, '');
+		last = match.index + match[0].length;
+
+		inner = match[1].trim();
+		char = inner.charAt(0);
+
+		if (char === '!') {
+			// comment, continue
+		} else if (char === '#') {
+			close();
+			[, action, inner] = /^#\s*(\w[\w\d]+)\s*([^]*)/.exec(inner);
+
+			if (action === 'expect') {
+				inner.split(/[\n\r\s\t]*,[\n\r\s\t]*/g).forEach(key => {
+					initials.add(key);
+				});
+			} else if (action === 'var') {
+				num = inner.indexOf('=');
+				tmp = inner.substring(0, num++).trim();
+				inner = inner.substring(num).trim().replace(/[;]$/, '');
+				txt += `var ${tmp}=${inner};`;
+			} else if (action === 'each') {
+				num = inner.indexOf(' as ');
+				stack.push(action);
+				if (!~num) {
+					txt += `for(var i=0,$$a=${inner};i<$$a.length;i++){`;
+				} else {
+					tmp = inner.substring(0, num).trim();
+					inner = inner.substring(num + 4).trim();
+					let [item, idx='i'] = inner.replace(/[()\s]/g, '').split(','); // (item, idx?)
+					txt += `for(var ${idx}=0,${item},$$a=${tmp};${idx}<$$a.length;${idx}++){${item}=$$a[${idx}];`;
+				}
+			} else if (action === 'if') {
+				txt += `if(${inner}){`;
+				stack.push(action);
+			} else if (action === 'elif') {
+				txt += `}else if(${inner}){`;
+			} else if (action === 'else') {
+				txt += `}else{`;
+			} else if (action in extra) {
+				if (inner) {
+					tmp = [];
+					// parse arguments, `defer=true` -> `{ defer: true }`
+					while (match = ARGS.exec(inner)) tmp.push(match[1] + ':' + match[2]);
+					inner = tmp.length ? '{' + tmp.join() + '}' : '';
+				}
+				inner = inner || '{}';
+				tmp = options.async ? 'await ' : '';
+				wip += '${' + tmp + '$$2.' + action + '(' + inner + ',$$2)}';
+			} else {
+				throw new Error(`Unknown "${action}" block`);
+			}
+		} else if (char === '/') {
+			action = inner.substring(1);
+			inner = stack.pop();
+			close();
+			if (action === inner) txt += '}';
+			else throw new Error(`Expected to close "${inner}" block; closed "${action}" instead`);
+		} else if (match[0].charAt(2) === '{') {
+			wip += '${' + inner + '}'; // {{{ raw }}}
+		} else {
+			wip += '${$$1(' + inner + ')}';
+		}
+	}
+
+	if (stack.length > 0) {
+		throw new Error(`Unterminated "${stack.pop()}" block`);
+	}
+
+	if (last < input.length) {
+		wip += input.substring(last).replace(ENDLINES, '');
+	}
+
+	close();
+
+	tmp = initials.size ? `{${ [...initials].join() }}=$$3,x` : ' x';
+	return `var${tmp + txt}return x`;
+}
+
+export function esc(value) {
+	if (typeof value !== 'string') return value;
+	let last=ESCAPE.lastIndex=0, tmp=0, out='';
+	while (ESCAPE.test(value)) {
+		tmp = ESCAPE.lastIndex - 1;
+		out += value.substring(last, tmp) + CHARS[value[tmp]];
+		last = tmp + 1;
+	}
+	return out + value.substring(last);
+}
+
+export function compile(input, options={}) {
+	return new (options.async ? (async()=>{}).constructor : Function)(
+		'$$1', '$$2', '$$3', gen(input, options)
+	).bind(0, options.escape || esc, options.blocks);
+}
+
+export function transform(input, options={}) {
+	return (
+		options.format === 'cjs'
+		? 'var $$1=require("tempura").esc;module.exports='
+		: 'import{esc as $$1}from"tempura";export default '
+	) + (
+		options.async ? 'async ' : ''
+	) + 'function($$3,$$2){'+gen(input, options)+'}';
+}
diff --git a/src/index.md b/src/index.md
index 7aead80..c65d784 100644
--- a/src/index.md
+++ b/src/index.md
@@ -28,18 +28,20 @@ support: true
 {% include 'card.html' %}
 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
-<script>
+<script type="module">
+
+  import * as apis from "./assets/javascript/apis.js";
+
   $(document).ready(function(){
-    var newData = false;
-    if (window.location.href.indexOf('nd=')>=0) newData = true;
     $.ajax({
       type: "GET",
-      url: (newData ? "https://raw.githubusercontent.com/APIs-guru/openapi-directory/gh-pages/v2/metrics.json" : "https://api.apis.guru/v2/metrics.json"),
+      url: "https://api.apis.guru/v2/metrics.json",
       dataType: 'json',
       cache: true,
       success: function (data) {
         $('#numAPIs').text(data.numAPIs.toLocaleString());
       }
     });
+    apis.loadAPIs();
   });
 </script>

From 3e90a9623ebebbf04c950f630bfb69746fbbe4ae Mon Sep 17 00:00:00 2001
From: Mike Ralphson <mike.ralphson@gmail.com>
Date: Tue, 28 Mar 2023 22:28:30 +0100
Subject: [PATCH 2/2] feat: add ChatGPT plugin manifest

---
 .eleventy.js                   |  1 +
 src/.well-known/ai-plugin.json | 18 ++++++++++++++++++
 2 files changed, 19 insertions(+)
 create mode 100644 src/.well-known/ai-plugin.json

diff --git a/.eleventy.js b/.eleventy.js
index e343c5b..b66e38a 100644
--- a/.eleventy.js
+++ b/.eleventy.js
@@ -7,6 +7,7 @@ module.exports = function(eleventyConfig) {
   eleventyConfig.addPassthroughCopy("src/assets");
   eleventyConfig.addPassthroughCopy("src/.nojekyll");
   eleventyConfig.addPassthroughCopy("src/robots.txt");
+  eleventyConfig.addPassthroughCopy("src/.well-known/ai-plugin.json");
   return {
     dir: {
       // ⚠️ These values are both relative to your input directory.
diff --git a/src/.well-known/ai-plugin.json b/src/.well-known/ai-plugin.json
new file mode 100644
index 0000000..1f3f82f
--- /dev/null
+++ b/src/.well-known/ai-plugin.json
@@ -0,0 +1,18 @@
+{
+  "schema_version": "v1",
+  "name_for_human": "APIs.guru Plugin",
+  "name_for_model": "apis.giru",
+  "description_for_human": "Plugin for accessing APIs.guru OpenAPI Directory.",
+  "description_for_model": "Plugin for accessing APIs.guru OpenAPI Directory.",
+  "auth": {
+    "type": "none"
+  },
+  "api": {
+    "type": "openapi",
+    "url": "https://api.apis.guru/v2/openapi.yaml",
+    "is_user_authenticated": false
+  },
+  "logo_url": "https://api.apis.guru/logo.svg",
+  "contact_email": "mike.ralphson@gmail.com",
+  "legal_info_url": "https://apis.guru"
+}