{"id":"87deeb645328fb3b","slug":"notebook-input","first_public_version":485,"paused_version":489,"likes":10,"publish_level":"live_unlisted","forks":0,"fork_of":null,"has_importers":true,"thumbnail":"3b5596b1002b2f7b87e00cc7142602db312b5f08b09f52d646540b19ccef40cb","default_thumbnail":"75f22ac154b8d813dbdcc59e6a06b01cd8d2dc37eb242156636ce0e61bb9caff","update_time":"2021-07-05T12:40:28.790Z","publish_time":"2023-01-12T00:48:44.767Z","publish_version":496,"latest_version":496,"roles":[],"sharing":null,"owner":{"id":"898d35f8860c7f03","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","login":"mootari","name":"Fabian Iwand","bio":"Web dev and tinkerer.","home_url":"https://twitter.com/mootari","type":"team","tier":"starter_2024"},"creator":{"id":"07362516b5994994","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","login":"mootari","name":"Fabian Iwand","bio":"Web dev and tinkerer.","home_url":"https://mootari.de/","tier":"pro"},"authors":[{"id":"07362516b5994994","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","name":"Fabian Iwand","login":"mootari","bio":"Web dev and tinkerer.","home_url":"https://mootari.de/","tier":"pro","approved":true,"description":""}],"files":[],"comments":[{"id":"e97befd845955d67","content":"I sent a suggestion but auto formatting, you can add a {headers:{\"Range\":\"bytes=0-499\"}} to the fetch options to grab just the front matter only","node_id":78,"create_time":"2022-02-25T10:23:42.489Z","update_time":null,"resolved":true,"user":{"id":"5215f6ec4a999d40","avatar_url":"https://avatars.observableusercontent.com/avatar/47327a8bc1966f2186dcb3ebf4b7ee6e4e7ab9a5c2a07405aff57200ea778f71","login":"tomlarkworthy","name":"Tom Larkworthy","bio":"Tech Lead at Taktile. ex Firebase, Google.\n🦋 larkworthy.bsky.social","home_url":"https://bsky.app/profile/larkworthy.bsky.social","tier":"pro"}},{"id":"e64075b8241daece","content":"Thanks for letting me know! The original implementation used Range requests (the reason why https://observablehq.com/@mootari/toolbox#readChunks has readChunks), but those weren't supported by Observable at the time.\n\nSeems support might have been added when https://github.com/observablehq/feedback/issues/283 was resolved.","node_id":78,"create_time":"2022-02-25T11:57:24.365Z","update_time":null,"resolved":true,"user":{"id":"07362516b5994994","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","login":"mootari","name":"Fabian Iwand","bio":"Web dev and tinkerer.","home_url":"https://mootari.de/","tier":"pro"}},{"id":"5c45b92aa3800a37","content":"Had to disable Range requests again, because they break preflight requests in Firefox and Safari.","node_id":78,"create_time":"2022-03-14T22:31:00.782Z","update_time":null,"resolved":true,"user":{"id":"07362516b5994994","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","login":"mootari","name":"Fabian Iwand","bio":"Web dev and tinkerer.","home_url":"https://mootari.de/","tier":"pro"}},{"id":"7c4b8d74c5bc7a44","content":":'(","node_id":78,"create_time":"2022-03-17T18:24:16.341Z","update_time":null,"resolved":true,"user":{"id":"5215f6ec4a999d40","avatar_url":"https://avatars.observableusercontent.com/avatar/47327a8bc1966f2186dcb3ebf4b7ee6e4e7ab9a5c2a07405aff57200ea778f71","login":"tomlarkworthy","name":"Tom Larkworthy","bio":"Tech Lead at Taktile. ex Firebase, Google.\n🦋 larkworthy.bsky.social","home_url":"https://bsky.app/profile/larkworthy.bsky.social","tier":"pro"}}],"commenting_lock":null,"suggestions_to":[],"suggestion_from":null,"collections":[{"id":"cd59e33651ec44ac","type":"public","slug":"observable","title":"Observable Things","description":"Tools and techniques that are mostly useful in the context of Observable.","update_time":"2023-03-04T11:10:46.363Z","pinned":false,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"ef828a4a84a0a670ecbe429c8a6b2ea256a143b47653c80e1db52bfc5d491682","thumbnail":"ef828a4a84a0a670ecbe429c8a6b2ea256a143b47653c80e1db52bfc5d491682","listing_count":35,"parent_collection_count":0,"owner":{"id":"898d35f8860c7f03","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","login":"mootari","name":"Fabian Iwand","bio":"Web dev and tinkerer.","home_url":"https://twitter.com/mootari","type":"team","tier":"starter_2024"}},{"id":"6d9c7f601029e6bf","type":"public","slug":"widgets","title":"Widgets and helpers","description":"","update_time":"2022-05-25T11:01:26.509Z","pinned":false,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"5e800ed130944cc98795ab965f46e5df27cde20e4ecdc7ce95fe5b79c125727f","thumbnail":"5e800ed130944cc98795ab965f46e5df27cde20e4ecdc7ce95fe5b79c125727f","listing_count":35,"parent_collection_count":0,"owner":{"id":"898d35f8860c7f03","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","login":"mootari","name":"Fabian Iwand","bio":"Web dev and tinkerer.","home_url":"https://twitter.com/mootari","type":"team","tier":"starter_2024"}}],"version":252,"title":"Notebook Input","license":null,"copyright":"","nodes":[{"id":0,"value":"md`# Notebook Input\n\nAn input widget that parses various formats of notebook identifiers and optionally retrieves ID and metadata.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":16,"value":"viewof data = notebookInput({\n  value: '@mootari/notebook-input',\n})","pinned":true,"mode":"js","data":null,"name":null},{"id":211,"value":"html`<table>${Object.entries(data).map(([k, v]) => html`<tr><th>${k}</th><td>${DOM.text(v)}`)}`","pinned":false,"mode":"js","data":null,"name":null},{"id":72,"value":"data","pinned":true,"mode":"js","data":null,"name":null},{"id":203,"value":"`${data}`","pinned":true,"mode":"js","data":null,"name":null},{"id":221,"value":"md`---`","pinned":false,"mode":"js","data":null,"name":null},{"id":5,"value":"function notebookInput(options = {}) {\n  const {\n    submit: submitLabel = 'Submit',\n    value = '',\n    ...fetchMetaOptions\n  } = options;\n  \n  const id = DOM.uid().id,\n        // Todo: butt-ugly form is butt-ugly.\n        $style = html`<style>\n#${id} input:invalid { color: red }\n#${id} { font-family: var(--sans-serif) }\n#${id} input, #${id} button, #${id} output {font-family: inherit }\n#${id} output { display: inline-block; color: red; padding: 0; font-size: 13.333px}\n        `,\n        $submit = html`<button>${DOM.text(submitLabel)}`,\n        $input = html`<input type=text required placeholder=\"Notebook ID, slug or URL\">`,\n        $error = html`<output>`,\n        $form = html`<form id=${id}>${$input} ${$submit} ${$error}${$style}`,\n        setValidity = msg => {\n          if(arguments.length) $input.setCustomValidity(msg);\n          // Required when setting custom validation on submit.\n          setTimeout(() => $error.value = $input.validationMessage, 0);\n          $submit.disabled = msg.length > 0;\n        };\n  \n  $input.value = value;\n  $input.oninvalid = e => e.preventDefault();\n  $input.oninput = e => {\n    if(e) e.stopPropagation();\n    setValidity('');\n  };\n  \n  $form.onsubmit = e => {\n    if(e) {\n      e.stopPropagation();\n      e.preventDefault();\n    }\n    \n    if(!regIdentifier.test($input.value)) {\n      setValidity('Invalid format.');\n      return;\n    }\n    // Disable form while querying.\n    $input.disabled = true;\n    $submit.disabled = true;\n    fetchMeta($input.value, fetchMetaOptions)\n      .then(data => {\n        const path = data.url ? new URL(data.url).pathname.slice(1) : null;\n        if(data) {\n          Object.defineProperty(data, 'toString', {value: () => path});\n        }\n        $form.value = data;\n        $form.dispatchEvent(new Event('input'));\n      })\n      .catch(e => {\n        setValidity('Notebook not found or inaccessible.');\n      })\n      .finally((v) => {\n        $input.disabled = false;\n        $submit.disabled = false;\n      })\n  };\n  \n  $input.oninput();\n  if($input.checkValidity()) $form.onsubmit();\n  \n  return $form;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":6,"value":"// A pattern that matches notebook URLs, slugs and IDs in various formats.\n  // Named search groups:\n  // - \"id\" (hexadecimal notebook ID),\n  // - \"user\" (user/team name without @),\n  // - \"slug\" (notebook URL after user/team name)\n  // - \"version\" (pinned version)\nregIdentifier = new RegExp('^'\n                     + '(?:(?:https:\\/\\/observablehq.com)?\\/)?'\n                     + '(?:'\n                     + '(?:d\\/)?(?<id>[a-f0-9]{16})'\n                     + '|'\n                     + '@(?<user>[0-9a-z-_]{2,})\\/(?<slug>[0-9a-z-_]{1,}(?:\\/\\\\d+)?)'\n                     + ')'\n                     + '(?:@(?<version>\\\\d+))?'\n                     + '([?#]|$)')","pinned":false,"mode":"js","data":null,"name":null},{"id":60,"value":"regV1Source = new RegExp('^'\n                       + '\\/\\/ URL: (?<url>[^\\\\n]+)\\\\n'\n                       + '\\/\\/ Title: (?<title>[^\\\\n]+)\\\\n'\n                       + '\\/\\/ Author: (?<name>.+?) \\\\(@(?<login>[^\\\\)]+)\\\\)\\\\n'\n                       + '\\/\\/ Version: (?<version>\\\\d+)\\\\n'\n                       + '\\/\\/ Runtime version: 1\\\\n'\n                       + '\\\\n'\n                       + 'const m0 = \\\\{\\\\n'\n                       + '  id: \"(?<id>[a-f0-9]{16})')","pinned":false,"mode":"js","data":null,"name":null},{"id":78,"value":"async function fetchMeta(identifier, {fallback = false, apiKey = null} = {}) {\n  const match = identifier.match(regIdentifier);\n  if(!match) return null;\n  \n  const {id, user, slug, version} = match.groups,\n        path = id ? `d/${id}` : `@${user}/${slug}`,\n        suffix = version ? `@${version}` : '',\n        params = new URLSearchParams([['v', 1],\n          ...(apiKey == null ? [] : [['api_key', apiKey]]),\n        ]);\n\n  return fetch(`https://api.observablehq.com/${path}${suffix}.js?${params}`)\n    .then(r => r.text())\n    .then(s => s.match(regV1Source).groups)\n    .then(d => ({...deriveMeta(d.url), ...d}))\n    .catch(e => {\n      if(fallback) return {id, user, slug, version};\n      throw e;\n    })\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":227,"value":"function deriveMeta(url) {\n  const path = new URL(url).pathname.slice(1);\n  return {\n    slug: path.replace(/^d\\//, ''),\n    module: `https://api.observablehq.com/${path}.js?v=3`,\n  };\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":27,"value":"html`<table>${\n[\n  'abcdef0123456789',\n  'abcdef0123456789@456',\n  'd/abcdef0123456789',\n  '/d/abcdef0123456789',\n  'https://observablehq.com/d/abcdef0123456789',\n  'https://observablehq.com/@login/123-notebook-slug',\n  'https://observablehq.com/@login/123-notebook-slug/123',\n  'https://observablehq.com/@login/123-notebook-slug/123@456',\n].map(s => html`<tr><td>${regIdentifier.test(s) ? '√' : 'x'}</td><td>${DOM.text(s)}</td>`)}`","pinned":false,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}