[{"data":1,"prerenderedAt":1886},["ShallowReactive",2],{"post-to-outerbase-with-bun-toastui-editor-and-chatgpt":3},{"id":4,"title":5,"body":6,"canonicalUrl":1870,"cover":1871,"date":1872,"description":1873,"draft":1874,"extension":1875,"hashnodeId":1876,"meta":1877,"navigation":267,"path":1878,"seo":1879,"slug":1880,"stem":1880,"tags":1881,"__hash__":1885},"posts\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt.md","To Outerbase with Bun, ToastUI Editor and ChatGPT",{"type":7,"value":8,"toc":1854},"minimark",[9,37,42,56,59,72,75,79,83,86,91,102,117,123,135,198,201,209,216,438,441,466,470,473,484,542,545,552,556,559,562,605,616,641,644,655,688,691,706,711,717,724,728,735,738,744,747,841,844,848,851,870,885,1022,1029,1033,1036,1182,1185,1281,1284,1456,1459,1521,1525,1528,1531,1611,1614,1699,1702,1798,1801,1805,1816,1820,1823,1826,1829,1831,1835,1838,1841,1847,1850],[10,11,12,13,17,18,21,22,25,26,29,30,36],"p",{},"This article is about exploring the new talk of the town, ",[14,15,16],"code",{},"bun",", getting to know ",[14,19,20],{},"Outerbase",", and hanging out with old buddies ",[14,23,24],{},"markdown"," and ",[14,27,28],{},"ChatGPT",". If you follow along, you'll get to know how the oven was heated, to bake fresh plugins for ",[31,32,20],"a",{"href":33,"rel":34},"https:\u002F\u002Fouterbase.com\u002F",[35],"nofollow",".",[38,39,41],"h2",{"id":40},"introduction","Introduction",[10,43,44,45,49,50,55],{},"When I learnt about the ",[31,46,20],{"href":47,"rel":48},"https:\u002F\u002Fbeta.outerbase.com\u002F",[35]," hackathon on ",[31,51,54],{"href":52,"rel":53},"https:\u002F\u002Fhashnode.com\u002F",[35],"Hashnode"," I was intrigued about it. I thought it was another database, \"base\" being the operative word. But I was only half right, or maybe half wrong, it depends on whom you're asking. Outerbase is an interface to your data (currently only residing in some relational databases), but it adds a lot of bells and whistles to make it interesting.",[10,57,58],{},"Some of the features include:",[60,61,62,66,69],"ol",{},[63,64,65],"li",{},"Commands: These are functions in the cloud (Lambda functions?) that can talk to your database. You can create a chain of nodes to handle different steps of the process (AWS Step functions? Of course, it is not there yet but maybe soon...)",[63,67,68],{},"EZQL: It enables you to ask questions to your database in plain text. No more making your own SQL queries.",[63,70,71],{},"Plugins: Your data tables are much more than a spreadsheet, Outerbase plugins enable you to visualize that. You can add different plugins for different types of data, and interact with it in a way that feels native.",[10,73,74],{},"The last feature is what caught my fancy, and I decided to focus on that for this hackathon. Here is a short video demo of the plugins that I created.",[76,77],"media-embed",{"url":78},"https:\u002F\u002Fyoutu.be\u002FaErH1bOy73o",[38,80,82],{"id":81},"preparing-the-base-with-bun","Preparing the base with Bun",[10,84,85],{},"Since I decided to focus on plugins, I wanted a way to quickly create the basic template on top of which I could build upon. One way was to just get the template from the Outerbase repo and copy-paste it to create more of it. But where is the fun in that? There is a saying, \"Automate it, silly.\" (Even if it takes you days to build that automation). So that is the path I took.",[87,88,90],"h3",{"id":89},"creating-templates","Creating templates",[10,92,93,94,97,98,101],{},"You can create local templates (and maybe publish them later on) with ",[14,95,96],{},"Bun"," and then run a simple command to use that template. Your local templates should be present inside a ",[14,99,100],{},".bun-create"," folder in the following paths",[103,104,105,111],"ul",{},[63,106,107,110],{},[14,108,109],{},"$HOME\u002F.bun-create\u002F\u003Cname>",": global templates",[63,112,113,116],{},[14,114,115],{},"\u003Cproject root>\u002F.bun-create\u002F\u003Cname>",": project-specific templates",[10,118,119,122],{},[14,120,121],{},"\u003Cname>"," is the template\u002Ffolder name you want to use. Drop your template files into the template folder and run the following command to use that template",[124,125,127,131],"div",{"dataNodeType":126},"callout",[124,128,130],{"dataNodeType":129},"callout-emoji","💡",[124,132,134],{"dataNodeType":133},"callout-text","Using a local template will overwrite the destination folder, so make sure that it doesn't exist, or is empty.",[136,137,142],"pre",{"className":138,"code":139,"language":140,"meta":141,"style":141},"language-bash shiki shiki-themes github-light github-dark","# Notice the \".\u002F\" in the beginning. Without that it looks \n# for the template on Github (bug). \nbun create .\u002F\u003Ctemplate-name> \u003Cdestination>\n","bash","",[14,143,144,153,159],{"__ignoreMap":141},[145,146,149],"span",{"class":147,"line":148},"line",1,[145,150,152],{"class":151},"sJ8bj","# Notice the \".\u002F\" in the beginning. Without that it looks \n",[145,154,156],{"class":147,"line":155},2,[145,157,158],{"class":151},"# for the template on Github (bug). \n",[145,160,162,165,169,172,176,179,183,186,189,192,195],{"class":147,"line":161},3,[145,163,16],{"class":164},"sScJk",[145,166,168],{"class":167},"sZZnC"," create",[145,170,171],{"class":167}," .\u002F",[145,173,175],{"class":174},"szBVR","\u003C",[145,177,178],{"class":167},"template-nam",[145,180,182],{"class":181},"sVt8B","e",[145,184,185],{"class":174},">",[145,187,188],{"class":174}," \u003C",[145,190,191],{"class":167},"destinatio",[145,193,194],{"class":181},"n",[145,196,197],{"class":174},">\n",[10,199,200],{},"There are two types of Outerbase plugins;",[103,202,203,206],{},[63,204,205],{},"Table plugins: these work on the whole database table, and",[63,207,208],{},"Cell plugins: made for working with a cell but applied on a column so that they're available to all the cells of that column",[10,210,211,212,215],{},"An Outerbase plugin consists of at most three views; 1. The configuration view, 2. The data view, and 3. The data editor\u002Fdialog view. These views are created using ",[14,213,214],{},"Web Components"," (Custom HTML Elements). A basic Outerbase component can be represented as follows",[136,217,221],{"className":218,"code":219,"language":220,"meta":141,"style":141},"language-javascript shiki shiki-themes github-light github-dark","const templateEditor = document.createElement(\"template\");\ntemplateEditor.innerHTML = `\n\u003Cstyle>\n  #container {\n    max-width: 320px;\n  }\n\u003C\u002Fstyle>\n\n\u003Cdiv id=\"container\">\u003C\u002Fdiv>\n`;\n\nclass OuterbasePluginCellEditor extends HTMLElement {\n  static get observedAttributes() {\n    return [...observed_attributes];\n  }\n\n  config = new OuterbasePluginConfig({});\n\n  constructor() {\n    super();\n\n    this.shadow = this.attachShadow({ mode: \"open\" });\n    this.shadow.appendChild(\n      templateEditor.content.cloneNode(true)\n    );\n  }\n\n  connectedCallback() {\n    this.config = new OuterbasePluginConfig(\n      decodeAttributeByName(this, \"configuration\")\n    );\n\n    this.config.cellValue = this.getAttribute(\"cellvalue\");\n    this.render();\n  }\n\n  render() {}\n}\n","javascript",[14,222,223,228,233,238,244,250,256,262,269,275,281,286,292,298,304,309,314,320,325,331,337,342,348,354,360,366,371,376,382,388,394,399,404,410,416,421,426,432],{"__ignoreMap":141},[145,224,225],{"class":147,"line":148},[145,226,227],{},"const templateEditor = document.createElement(\"template\");\n",[145,229,230],{"class":147,"line":155},[145,231,232],{},"templateEditor.innerHTML = `\n",[145,234,235],{"class":147,"line":161},[145,236,237],{},"\u003Cstyle>\n",[145,239,241],{"class":147,"line":240},4,[145,242,243],{},"  #container {\n",[145,245,247],{"class":147,"line":246},5,[145,248,249],{},"    max-width: 320px;\n",[145,251,253],{"class":147,"line":252},6,[145,254,255],{},"  }\n",[145,257,259],{"class":147,"line":258},7,[145,260,261],{},"\u003C\u002Fstyle>\n",[145,263,265],{"class":147,"line":264},8,[145,266,268],{"emptyLinePlaceholder":267},true,"\n",[145,270,272],{"class":147,"line":271},9,[145,273,274],{},"\u003Cdiv id=\"container\">\u003C\u002Fdiv>\n",[145,276,278],{"class":147,"line":277},10,[145,279,280],{},"`;\n",[145,282,284],{"class":147,"line":283},11,[145,285,268],{"emptyLinePlaceholder":267},[145,287,289],{"class":147,"line":288},12,[145,290,291],{},"class OuterbasePluginCellEditor extends HTMLElement {\n",[145,293,295],{"class":147,"line":294},13,[145,296,297],{},"  static get observedAttributes() {\n",[145,299,301],{"class":147,"line":300},14,[145,302,303],{},"    return [...observed_attributes];\n",[145,305,307],{"class":147,"line":306},15,[145,308,255],{},[145,310,312],{"class":147,"line":311},16,[145,313,268],{"emptyLinePlaceholder":267},[145,315,317],{"class":147,"line":316},17,[145,318,319],{},"  config = new OuterbasePluginConfig({});\n",[145,321,323],{"class":147,"line":322},18,[145,324,268],{"emptyLinePlaceholder":267},[145,326,328],{"class":147,"line":327},19,[145,329,330],{},"  constructor() {\n",[145,332,334],{"class":147,"line":333},20,[145,335,336],{},"    super();\n",[145,338,340],{"class":147,"line":339},21,[145,341,268],{"emptyLinePlaceholder":267},[145,343,345],{"class":147,"line":344},22,[145,346,347],{},"    this.shadow = this.attachShadow({ mode: \"open\" });\n",[145,349,351],{"class":147,"line":350},23,[145,352,353],{},"    this.shadow.appendChild(\n",[145,355,357],{"class":147,"line":356},24,[145,358,359],{},"      templateEditor.content.cloneNode(true)\n",[145,361,363],{"class":147,"line":362},25,[145,364,365],{},"    );\n",[145,367,369],{"class":147,"line":368},26,[145,370,255],{},[145,372,374],{"class":147,"line":373},27,[145,375,268],{"emptyLinePlaceholder":267},[145,377,379],{"class":147,"line":378},28,[145,380,381],{},"  connectedCallback() {\n",[145,383,385],{"class":147,"line":384},29,[145,386,387],{},"    this.config = new OuterbasePluginConfig(\n",[145,389,391],{"class":147,"line":390},30,[145,392,393],{},"      decodeAttributeByName(this, \"configuration\")\n",[145,395,397],{"class":147,"line":396},31,[145,398,365],{},[145,400,402],{"class":147,"line":401},32,[145,403,268],{"emptyLinePlaceholder":267},[145,405,407],{"class":147,"line":406},33,[145,408,409],{},"    this.config.cellValue = this.getAttribute(\"cellvalue\");\n",[145,411,413],{"class":147,"line":412},34,[145,414,415],{},"    this.render();\n",[145,417,419],{"class":147,"line":418},35,[145,420,255],{},[145,422,424],{"class":147,"line":423},36,[145,425,268],{"emptyLinePlaceholder":267},[145,427,429],{"class":147,"line":428},37,[145,430,431],{},"  render() {}\n",[145,433,435],{"class":147,"line":434},38,[145,436,437],{},"}\n",[10,439,440],{},"Before we can use this component we need to register this component with the window",[136,442,444],{"className":218,"code":443,"language":220,"meta":141,"style":141},"window.customElements.define(\n  \"outerbase-plugin-cell-editor\",\n  OuterbasePluginCellEditor\n);\n",[14,445,446,451,456,461],{"__ignoreMap":141},[145,447,448],{"class":147,"line":148},[145,449,450],{},"window.customElements.define(\n",[145,452,453],{"class":147,"line":155},[145,454,455],{},"  \"outerbase-plugin-cell-editor\",\n",[145,457,458],{"class":147,"line":161},[145,459,460],{},"  OuterbasePluginCellEditor\n",[145,462,463],{"class":147,"line":240},[145,464,465],{},");\n",[38,467,469],{"id":468},"the-first-plugin-audio-player","The first plugin: Audio Player",[10,471,472],{},"If your data consists of audio urls, wouldn't it be cool to play the audio from the database view itself? This Outerbase cell plugin allows you to do exactly that.",[10,474,475,476,479,480,483],{},"All the magic of this plugin resides in its ",[14,477,478],{},"Editor"," view. We simply attach an HTML audio element to the view. Set the audio src and load it. And our audio link is ready to play. Below is the updated ",[14,481,482],{},"render"," method shown earlier.",[136,485,487],{"className":218,"code":486,"language":220,"meta":141,"style":141},"render() {\n  const srcUrl = this.getAttribute(\"cellvalue\");\n  if (srcUrl) {\n    this.shadow.getElementById(\n      \"container\"\n    ).innerHTML = `\u003Caudio id=\"audio-player\" controls \u002F>`;\n    const player = this.shadow.getElementById(\"audio-player\");\n    player.src = srcUrl;\n    player.load();\n  }\n}\n",[14,488,489,494,499,504,509,514,519,524,529,534,538],{"__ignoreMap":141},[145,490,491],{"class":147,"line":148},[145,492,493],{},"render() {\n",[145,495,496],{"class":147,"line":155},[145,497,498],{},"  const srcUrl = this.getAttribute(\"cellvalue\");\n",[145,500,501],{"class":147,"line":161},[145,502,503],{},"  if (srcUrl) {\n",[145,505,506],{"class":147,"line":240},[145,507,508],{},"    this.shadow.getElementById(\n",[145,510,511],{"class":147,"line":246},[145,512,513],{},"      \"container\"\n",[145,515,516],{"class":147,"line":252},[145,517,518],{},"    ).innerHTML = `\u003Caudio id=\"audio-player\" controls \u002F>`;\n",[145,520,521],{"class":147,"line":258},[145,522,523],{},"    const player = this.shadow.getElementById(\"audio-player\");\n",[145,525,526],{"class":147,"line":264},[145,527,528],{},"    player.src = srcUrl;\n",[145,530,531],{"class":147,"line":271},[145,532,533],{},"    player.load();\n",[145,535,536],{"class":147,"line":277},[145,537,255],{},[145,539,540],{"class":147,"line":283},[145,541,437],{},[10,543,544],{},"The above code allows us to get this view (the audio player dialog)",[10,546,547],{},[548,549],"img",{"alt":550,"src":551},"audio player plugin view","\u002Fimages\u002Fposts\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt\u002Ff7eebf16-0ea5-4ecd-8f78-9f4cea51ff09-6dbb1da716.png",[38,553,555],{"id":554},"the-second-plugin-video-player","The second plugin: Video Player",[10,557,558],{},"Since we have an audio player for our database now, it is only logical to do the same thing for videos. But this is not that simple. Most of the video links you encounter are YouTube (and maybe some Vimeo) video links, so I set out to make that work.",[10,560,561],{},"Now, the URLs that we see in the browser's address bar (watch URLs) for YouTube videos are different from when you want to embed the YouTube player in your website. Assuming the database will store the watch URLs, we need to create the embed URLs. The below function does exactly that using a regex.",[136,563,565],{"className":218,"code":564,"language":220,"meta":141,"style":141},"getYouTubeEmbedUrl(url) {\n  const youtubeRegex =\n    \u002F^.*(youtu.be\\\u002F|v\\\u002F|u\\\u002F\\w\\\u002F|embed\\\u002F|watch\\?v=|\\&v=)([^#\\&\\?]*).*\u002F;\n  const match = url.match(youtubeRegex);\n  if (match && match[2].length == 11) {\n    return `https:\u002F\u002Fyoutube.com\u002Fembed\u002F${match[2]}`;\n  }\n}\n",[14,566,567,572,577,582,587,592,597,601],{"__ignoreMap":141},[145,568,569],{"class":147,"line":148},[145,570,571],{},"getYouTubeEmbedUrl(url) {\n",[145,573,574],{"class":147,"line":155},[145,575,576],{},"  const youtubeRegex =\n",[145,578,579],{"class":147,"line":161},[145,580,581],{},"    \u002F^.*(youtu.be\\\u002F|v\\\u002F|u\\\u002F\\w\\\u002F|embed\\\u002F|watch\\?v=|\\&v=)([^#\\&\\?]*).*\u002F;\n",[145,583,584],{"class":147,"line":240},[145,585,586],{},"  const match = url.match(youtubeRegex);\n",[145,588,589],{"class":147,"line":246},[145,590,591],{},"  if (match && match[2].length == 11) {\n",[145,593,594],{"class":147,"line":252},[145,595,596],{},"    return `https:\u002F\u002Fyoutube.com\u002Fembed\u002F${match[2]}`;\n",[145,598,599],{"class":147,"line":258},[145,600,255],{},[145,602,603],{"class":147,"line":264},[145,604,437],{},[10,606,607,608,611,612,615],{},"Now we can create an ",[14,609,610],{},"iframe",", set its ",[14,613,614],{},"src"," as this embed URL and our player should be ready. We do all this in the editor\u002Fdialog view of the plugin.",[136,617,619],{"className":218,"code":618,"language":220,"meta":141,"style":141},"this.shadow.getElementById(\"container\").innerHTML = \n    `\u003Ciframe id=\"video-player\" type=\"text\u002Fhtml\" width=\"360\" height=\"240\" frameborder=\"0\" \u002F>`;\nconst player = this.shadow.getElementById(\"video-player\");\nplayer.src = embedUrl;\n",[14,620,621,626,631,636],{"__ignoreMap":141},[145,622,623],{"class":147,"line":148},[145,624,625],{},"this.shadow.getElementById(\"container\").innerHTML = \n",[145,627,628],{"class":147,"line":155},[145,629,630],{},"    `\u003Ciframe id=\"video-player\" type=\"text\u002Fhtml\" width=\"360\" height=\"240\" frameborder=\"0\" \u002F>`;\n",[145,632,633],{"class":147,"line":161},[145,634,635],{},"const player = this.shadow.getElementById(\"video-player\");\n",[145,637,638],{"class":147,"line":240},[145,639,640],{},"player.src = embedUrl;\n",[10,642,643],{},"The above code works fine locally but when deployed to the Outerbase console it fails to load the player. The reason is Outerbase plugins run inside a sandboxed iframe to keep the data secure. The YouTube player needs to use the browser cache to store its assets but the configured sandbox permissions don't allow it, and the player fails to appear.",[10,645,646,647,654],{},"But the same technique can be utilized for other video hosting platforms, Vimeo being one of them. The problem with Vimeo is that it doesn't have well structured consistent URLs. So we use its ",[31,648,651],{"href":649,"rel":650},"https:\u002F\u002Fdeveloper.vimeo.com\u002Fapi\u002Foembed\u002Fvideos",[35],[14,652,653],{},"oEmbed APIs"," to fetch the correct URL.",[136,656,658],{"className":218,"code":657,"language":220,"meta":141,"style":141},"async getVimeoEmbedUrl(url) {\n  const res = await fetch(`https:\u002F\u002Fvimeo.com\u002Fapi\u002Foembed.json?url=${url}`);\n\n  const data = await res.json();\n  return `https:\u002F\u002Fplayer.vimeo.com\u002Fvideo\u002F${data.video_id}?title=0&byline=0&dnt=1`;\n}\n",[14,659,660,665,670,674,679,684],{"__ignoreMap":141},[145,661,662],{"class":147,"line":148},[145,663,664],{},"async getVimeoEmbedUrl(url) {\n",[145,666,667],{"class":147,"line":155},[145,668,669],{},"  const res = await fetch(`https:\u002F\u002Fvimeo.com\u002Fapi\u002Foembed.json?url=${url}`);\n",[145,671,672],{"class":147,"line":161},[145,673,268],{"emptyLinePlaceholder":267},[145,675,676],{"class":147,"line":240},[145,677,678],{},"  const data = await res.json();\n",[145,680,681],{"class":147,"line":246},[145,682,683],{},"  return `https:\u002F\u002Fplayer.vimeo.com\u002Fvideo\u002F${data.video_id}?title=0&byline=0&dnt=1`;\n",[145,685,686],{"class":147,"line":252},[145,687,437],{},[10,689,690],{},"And now we can use the same iframe player to play this video.",[124,692,693,695],{"dataNodeType":126},[124,694,130],{"dataNodeType":129},[124,696,697,698,701,702,705],{"dataNodeType":133},"Notice the query parameter ",[14,699,700],{},"&dnt=1"," in the created URL. Without that Vimeo player will also follow the YouTube player's way. ",[14,703,704],{},"dnt"," is \"do not track\". If it is 0 (the default value), the Vimeo player will try to use browser cookies and will fail as we're sandboxed.",[10,707,708,710],{},[14,709,704],{}," parameter saved the day for this plugin, else all that work would have come to nought. Here is the view we get from the player",[10,712,713],{},[548,714],{"alt":715,"src":716},"vimeo player view","\u002Fimages\u002Fposts\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt\u002F24d8e698-fc93-4d7a-8ea6-a084ac47e6a6-cf465f47b3.png",[10,718,719,720,723],{},"There is still one error present in the browser console for the Vimeo player, and that is for missing the ",[14,721,722],{},"presentation"," permission. This can be alleviated by sandbox=\"allow-presentation\" on the parent iframe, or maybe there is some other Vimeo URL query param that can help with that. I haven't explored further as even with the error in the console, the video can be played (The first player load doesn't work, but no problem afterwards).",[38,725,727],{"id":726},"the-third-plugin-markdown-editor","The third plugin: Markdown Editor",[10,729,730,731,734],{},"How cool it would be to create blog posts or write docs from the database view itself? This ",[14,732,733],{},"md-editor"," plugin is exactly what you need for all such cases.",[10,736,737],{},"Let's first take a peek at the plugins editor view",[10,739,740],{},[548,741],{"alt":742,"src":743},"md-editor view","\u002Fimages\u002Fposts\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt\u002F555b7179-bf05-404f-875a-ee6666f8ab31-0472254c41.png",[10,745,746],{},"This plugin is different from the other plugins we've seen so far, as here we need to use third-party scripts and CSS to achieve the goal. How do we go about this, and which markdown editor to integrate? These are important questions to answer. Let's tackle these questions one by one",[60,748,749,838],{},[63,750,751,752],{},"How to include third-party scripts and CSS: Even though we're using web components, these are still part of the DOM, so maybe we can create script and link tags dynamically to load these. An example would be as follows",[136,753,755],{"className":218,"code":754,"language":220,"meta":141,"style":141},"\u002F\u002F JavaScript\nfunction loadCSS(url) {\n  const link = document.createElement('link');\n  link.rel = 'stylesheet';\n  link.href = url;\n  document.head.appendChild(link);\n}\n\nfunction loadJS(url) {\n  const script = document.createElement('script');\n  script.src = url;\n  document.body.appendChild(script);\n}\n\n\u002F\u002F Example usage:\nloadCSS('https:\u002F\u002Fcdnjs.cloudflare.com\u002Fajax\u002Flibs\u002Fbootstrap\u002F4.6.0\u002Fcss\u002Fbootstrap.min.css');\nloadJS('https:\u002F\u002Fcode.jquery.com\u002Fjquery-3.6.0.min.js');\n",[14,756,757,762,767,772,777,782,787,791,795,800,805,810,815,819,823,828,833],{"__ignoreMap":141},[145,758,759],{"class":147,"line":148},[145,760,761],{},"\u002F\u002F JavaScript\n",[145,763,764],{"class":147,"line":155},[145,765,766],{},"function loadCSS(url) {\n",[145,768,769],{"class":147,"line":161},[145,770,771],{},"  const link = document.createElement('link');\n",[145,773,774],{"class":147,"line":240},[145,775,776],{},"  link.rel = 'stylesheet';\n",[145,778,779],{"class":147,"line":246},[145,780,781],{},"  link.href = url;\n",[145,783,784],{"class":147,"line":252},[145,785,786],{},"  document.head.appendChild(link);\n",[145,788,789],{"class":147,"line":258},[145,790,437],{},[145,792,793],{"class":147,"line":264},[145,794,268],{"emptyLinePlaceholder":267},[145,796,797],{"class":147,"line":271},[145,798,799],{},"function loadJS(url) {\n",[145,801,802],{"class":147,"line":277},[145,803,804],{},"  const script = document.createElement('script');\n",[145,806,807],{"class":147,"line":283},[145,808,809],{},"  script.src = url;\n",[145,811,812],{"class":147,"line":288},[145,813,814],{},"  document.body.appendChild(script);\n",[145,816,817],{"class":147,"line":294},[145,818,437],{},[145,820,821],{"class":147,"line":300},[145,822,268],{"emptyLinePlaceholder":267},[145,824,825],{"class":147,"line":306},[145,826,827],{},"\u002F\u002F Example usage:\n",[145,829,830],{"class":147,"line":311},[145,831,832],{},"loadCSS('https:\u002F\u002Fcdnjs.cloudflare.com\u002Fajax\u002Flibs\u002Fbootstrap\u002F4.6.0\u002Fcss\u002Fbootstrap.min.css');\n",[145,834,835],{"class":147,"line":316},[145,836,837],{},"loadJS('https:\u002F\u002Fcode.jquery.com\u002Fjquery-3.6.0.min.js');\n",[63,839,840],{},"Which editor to integrate: There are many markdown editors available, but we need something that can work with Web Components (in-browser). I also wanted to avoid any kind of bundling of the scripts with the plugin. This is because the Outerbase plugins can have only so much size, and markdown\u002FWYSIWYG editors are bulky. So we need one which is available from a CDN. I looked at and tried QuillJs (with QuillJs Markdown module), SimpleMDE and ToastUI Editor, and the latter one seemed relatively maintained and worked on the first try so that is the one we integrate.",[10,842,843],{},"Using the functions listed just above, if you try to load the scripts and CSS in an Outerbase plugin you'll get the gotcha moment of this plugin; you're not allowed to load third-party CSS because of the CSP (content security policy). What do we do now? Without the stylesheets, it is pointless to integrate the editor. Maybe we bundle the CSS with the plugin (bundling the script is still a NO, due to its size)?",[87,845,847],{"id":846},"bun-as-a-package-manager-bundler","Bun as a package manager & bundler",[10,849,850],{},"Now we go back to reading about bun. After reading the docs it became clear that Bun doesn't support bundling the CSS at present. It simply copies the CSS files to the outdir and renames their references. There are two ways out of this:",[60,852,853,867],{},[63,854,855,856],{},"Create a bun plugin to handle the CSS file bundling.",[60,857,858,861,864],{},[63,859,860],{},"Read the CSS files from node_modules",[63,862,863],{},"Minify using some third-party CSS minifier (no native CSS loader) and,",[63,865,866],{},"Inject as text into the final bundle",[63,868,869],{},"Simply get the minified CSS files from the CDN, and inject them as text into the plugin code (no bundling needed as the plugin code is very small)",[10,871,872,873,876,877,880,881,884],{},"So we pick the easy way out here and pick the second option. Run the ",[14,874,875],{},"bun init"," command to quickly create a ",[14,878,879],{},"package.json"," file (so that we can use bun APIs). Create a new file ",[14,882,883],{},"build.js"," for handling the tooling. This is the function which does what we need. Replacements is just an object where the keys are the identifiers we want to replace with the actual CSS styles.",[136,886,888],{"className":218,"code":887,"language":220,"meta":141,"style":141},"const bundle = async (replacements) => {\n  if (replacements) {\n    const indexFile = Bun.file(\"index.js\");\n    let indexFileText = await indexFile.text();\n\n    for (const key in replacements) {\n      \u002F\u002F Fetch the CSS file from the CDN using the URL\n      const res = await fetch(replacements[key]);\n      const fText = await res.text();\n\n      indexFileText = indexFileText.replace(key, fText);\n    }\n\n    createOutput(\"out\", indexFileText);\n  } else {\n    fs.cpSync(\"index.js\", \"out\u002Findex.js\");\n  }\n};\n\nconst createOutput = (dir, fileText) => {\n  const filePath = `${dir}\u002Findex.js`;\n  const directoryPath = path.dirname(filePath);\n  if (!fs.existsSync(directoryPath)) {\n    fs.mkdirSync(directoryPath, { recursive: true });\n  }\n\n  fs.writeFileSync(filePath, fileText);\n};\n",[14,889,890,895,900,905,910,914,919,924,929,934,938,943,948,952,957,962,967,971,976,980,985,990,995,1000,1005,1009,1013,1018],{"__ignoreMap":141},[145,891,892],{"class":147,"line":148},[145,893,894],{},"const bundle = async (replacements) => {\n",[145,896,897],{"class":147,"line":155},[145,898,899],{},"  if (replacements) {\n",[145,901,902],{"class":147,"line":161},[145,903,904],{},"    const indexFile = Bun.file(\"index.js\");\n",[145,906,907],{"class":147,"line":240},[145,908,909],{},"    let indexFileText = await indexFile.text();\n",[145,911,912],{"class":147,"line":246},[145,913,268],{"emptyLinePlaceholder":267},[145,915,916],{"class":147,"line":252},[145,917,918],{},"    for (const key in replacements) {\n",[145,920,921],{"class":147,"line":258},[145,922,923],{},"      \u002F\u002F Fetch the CSS file from the CDN using the URL\n",[145,925,926],{"class":147,"line":264},[145,927,928],{},"      const res = await fetch(replacements[key]);\n",[145,930,931],{"class":147,"line":271},[145,932,933],{},"      const fText = await res.text();\n",[145,935,936],{"class":147,"line":277},[145,937,268],{"emptyLinePlaceholder":267},[145,939,940],{"class":147,"line":283},[145,941,942],{},"      indexFileText = indexFileText.replace(key, fText);\n",[145,944,945],{"class":147,"line":288},[145,946,947],{},"    }\n",[145,949,950],{"class":147,"line":294},[145,951,268],{"emptyLinePlaceholder":267},[145,953,954],{"class":147,"line":300},[145,955,956],{},"    createOutput(\"out\", indexFileText);\n",[145,958,959],{"class":147,"line":306},[145,960,961],{},"  } else {\n",[145,963,964],{"class":147,"line":311},[145,965,966],{},"    fs.cpSync(\"index.js\", \"out\u002Findex.js\");\n",[145,968,969],{"class":147,"line":316},[145,970,255],{},[145,972,973],{"class":147,"line":322},[145,974,975],{},"};\n",[145,977,978],{"class":147,"line":327},[145,979,268],{"emptyLinePlaceholder":267},[145,981,982],{"class":147,"line":333},[145,983,984],{},"const createOutput = (dir, fileText) => {\n",[145,986,987],{"class":147,"line":339},[145,988,989],{},"  const filePath = `${dir}\u002Findex.js`;\n",[145,991,992],{"class":147,"line":344},[145,993,994],{},"  const directoryPath = path.dirname(filePath);\n",[145,996,997],{"class":147,"line":350},[145,998,999],{},"  if (!fs.existsSync(directoryPath)) {\n",[145,1001,1002],{"class":147,"line":356},[145,1003,1004],{},"    fs.mkdirSync(directoryPath, { recursive: true });\n",[145,1006,1007],{"class":147,"line":362},[145,1008,255],{},[145,1010,1011],{"class":147,"line":368},[145,1012,268],{"emptyLinePlaceholder":267},[145,1014,1015],{"class":147,"line":373},[145,1016,1017],{},"  fs.writeFileSync(filePath, fileText);\n",[145,1019,1020],{"class":147,"line":378},[145,1021,975],{},[10,1023,1024,1025,1028],{},"Since we're in the CLI realm now, added a little complexity to get the file names as CLI input (using ",[14,1026,1027],{},"commander","). You can check the Github repo for the code.",[87,1030,1032],{"id":1031},"coding-the-plugin","Coding the plugin",[10,1034,1035],{},"First of all, we load the script (remember the CSS is already injected into the plugin, in the style tag of the cell editor's shadow dom, to be precise).",[136,1037,1039],{"className":218,"code":1038,"language":220,"meta":141,"style":141},"loadToastUiEditor() {\n  const scriptSrc =\n    \"https:\u002F\u002Fuicdn.toast.com\u002Feditor\u002Flatest\u002Ftoastui-editor-all.min.js\";\n  \u002F\u002F Optimization to not load the script again \n  \u002F\u002F and again, as the editor is recreated every time\n  if (document.scripts) {\n    for (const script of document.scripts) {\n      if (script.src === scriptSrc) {\n        console.log(\"script already loaded, bail out\");\n        return;\n      }\n    }\n  }\n\n  const el = document.createElement(\"script\");\n  el.src = scriptSrc;\n\n  el.onload = () => {\n    this.render();\n  };\n\n  el.onerror = (event) => {\n    console.log(\"failed to load the script\", event);\n  };\n\n  \u002F\u002F We're adding the script to the document and not \n  \u002F\u002F the shadow dom, because the shadom dom is recreated\n  \u002F\u002F whenver the editor is opened\n  document.head.appendChild(el);\n}\n",[14,1040,1041,1046,1051,1056,1061,1066,1071,1076,1081,1086,1091,1096,1100,1104,1108,1113,1118,1122,1127,1131,1136,1140,1145,1150,1154,1158,1163,1168,1173,1178],{"__ignoreMap":141},[145,1042,1043],{"class":147,"line":148},[145,1044,1045],{},"loadToastUiEditor() {\n",[145,1047,1048],{"class":147,"line":155},[145,1049,1050],{},"  const scriptSrc =\n",[145,1052,1053],{"class":147,"line":161},[145,1054,1055],{},"    \"https:\u002F\u002Fuicdn.toast.com\u002Feditor\u002Flatest\u002Ftoastui-editor-all.min.js\";\n",[145,1057,1058],{"class":147,"line":240},[145,1059,1060],{},"  \u002F\u002F Optimization to not load the script again \n",[145,1062,1063],{"class":147,"line":246},[145,1064,1065],{},"  \u002F\u002F and again, as the editor is recreated every time\n",[145,1067,1068],{"class":147,"line":252},[145,1069,1070],{},"  if (document.scripts) {\n",[145,1072,1073],{"class":147,"line":258},[145,1074,1075],{},"    for (const script of document.scripts) {\n",[145,1077,1078],{"class":147,"line":264},[145,1079,1080],{},"      if (script.src === scriptSrc) {\n",[145,1082,1083],{"class":147,"line":271},[145,1084,1085],{},"        console.log(\"script already loaded, bail out\");\n",[145,1087,1088],{"class":147,"line":277},[145,1089,1090],{},"        return;\n",[145,1092,1093],{"class":147,"line":283},[145,1094,1095],{},"      }\n",[145,1097,1098],{"class":147,"line":288},[145,1099,947],{},[145,1101,1102],{"class":147,"line":294},[145,1103,255],{},[145,1105,1106],{"class":147,"line":300},[145,1107,268],{"emptyLinePlaceholder":267},[145,1109,1110],{"class":147,"line":306},[145,1111,1112],{},"  const el = document.createElement(\"script\");\n",[145,1114,1115],{"class":147,"line":311},[145,1116,1117],{},"  el.src = scriptSrc;\n",[145,1119,1120],{"class":147,"line":316},[145,1121,268],{"emptyLinePlaceholder":267},[145,1123,1124],{"class":147,"line":322},[145,1125,1126],{},"  el.onload = () => {\n",[145,1128,1129],{"class":147,"line":327},[145,1130,415],{},[145,1132,1133],{"class":147,"line":333},[145,1134,1135],{},"  };\n",[145,1137,1138],{"class":147,"line":339},[145,1139,268],{"emptyLinePlaceholder":267},[145,1141,1142],{"class":147,"line":344},[145,1143,1144],{},"  el.onerror = (event) => {\n",[145,1146,1147],{"class":147,"line":350},[145,1148,1149],{},"    console.log(\"failed to load the script\", event);\n",[145,1151,1152],{"class":147,"line":356},[145,1153,1135],{},[145,1155,1156],{"class":147,"line":362},[145,1157,268],{"emptyLinePlaceholder":267},[145,1159,1160],{"class":147,"line":368},[145,1161,1162],{},"  \u002F\u002F We're adding the script to the document and not \n",[145,1164,1165],{"class":147,"line":373},[145,1166,1167],{},"  \u002F\u002F the shadow dom, because the shadom dom is recreated\n",[145,1169,1170],{"class":147,"line":378},[145,1171,1172],{},"  \u002F\u002F whenver the editor is opened\n",[145,1174,1175],{"class":147,"line":384},[145,1176,1177],{},"  document.head.appendChild(el);\n",[145,1179,1180],{"class":147,"line":390},[145,1181,437],{},[10,1183,1184],{},"As soon as the script is loaded we are ready to show our markdown editor",[136,1186,1188],{"className":218,"code":1187,"language":220,"meta":141,"style":141},"render() {\n  try {\n    const Editor = toastui.Editor;\n    this.editor = new Editor({\n      el: this.shadow.querySelector(\"#editor\"),\n      height: \"420px\",\n      initialEditType: \"markdown\",\n      initialValue: this.getAttribute(\"cellvalue\"),\n      previewStyle: \"vertical\",\n      usageStatistics: false,\n      theme: this.config.theme,\n      events: { keydown: this.handleKeyDown },\n    });\n\n    this.setEditorPosition();\n  } catch (error) {\n    console.log(\"render error\", error);\n  }\n}\n",[14,1189,1190,1194,1199,1204,1209,1214,1219,1224,1229,1234,1239,1244,1249,1254,1258,1263,1268,1273,1277],{"__ignoreMap":141},[145,1191,1192],{"class":147,"line":148},[145,1193,493],{},[145,1195,1196],{"class":147,"line":155},[145,1197,1198],{},"  try {\n",[145,1200,1201],{"class":147,"line":161},[145,1202,1203],{},"    const Editor = toastui.Editor;\n",[145,1205,1206],{"class":147,"line":240},[145,1207,1208],{},"    this.editor = new Editor({\n",[145,1210,1211],{"class":147,"line":246},[145,1212,1213],{},"      el: this.shadow.querySelector(\"#editor\"),\n",[145,1215,1216],{"class":147,"line":252},[145,1217,1218],{},"      height: \"420px\",\n",[145,1220,1221],{"class":147,"line":258},[145,1222,1223],{},"      initialEditType: \"markdown\",\n",[145,1225,1226],{"class":147,"line":264},[145,1227,1228],{},"      initialValue: this.getAttribute(\"cellvalue\"),\n",[145,1230,1231],{"class":147,"line":271},[145,1232,1233],{},"      previewStyle: \"vertical\",\n",[145,1235,1236],{"class":147,"line":277},[145,1237,1238],{},"      usageStatistics: false,\n",[145,1240,1241],{"class":147,"line":283},[145,1242,1243],{},"      theme: this.config.theme,\n",[145,1245,1246],{"class":147,"line":288},[145,1247,1248],{},"      events: { keydown: this.handleKeyDown },\n",[145,1250,1251],{"class":147,"line":294},[145,1252,1253],{},"    });\n",[145,1255,1256],{"class":147,"line":300},[145,1257,268],{"emptyLinePlaceholder":267},[145,1259,1260],{"class":147,"line":306},[145,1261,1262],{},"    this.setEditorPosition();\n",[145,1264,1265],{"class":147,"line":311},[145,1266,1267],{},"  } catch (error) {\n",[145,1269,1270],{"class":147,"line":316},[145,1271,1272],{},"    console.log(\"render error\", error);\n",[145,1274,1275],{"class":147,"line":322},[145,1276,255],{},[145,1278,1279],{"class":147,"line":327},[145,1280,437],{},[10,1282,1283],{},"Many things are going on here most of which are self-explanatory, I'll briefly touch upon the important points",[60,1285,1286,1298,1381,1408],{},[63,1287,1288,1289],{},"We open the editor with whatever content the cell was holding",[136,1290,1292],{"className":218,"code":1291,"language":220,"meta":141,"style":141},"initialValue: this.getAttribute(\"cellvalue\")\n",[14,1293,1294],{"__ignoreMap":141},[145,1295,1296],{"class":147,"line":148},[145,1297,1291],{},[63,1299,1300,1301],{},"Because of the way cell plugins have been designed, they pop up near the cell to which they're attached. For our plugin, we need a centred dialog. We achieve that through plain old DOM manipulation",[136,1302,1304],{"className":218,"code":1303,"language":220,"meta":141,"style":141},"setEditorPosition() {\n  const agPopUpChild = document.querySelector(\".ag-popup-child\");\n  const container = this.shadow.getElementById(\"container\");\n\n  setTimeout(() => {\n    agPopUpChild.style.left = `${\n      (window.innerWidth - container.offsetWidth) \u002F 2\n    }px`;\n    \u002F\u002F agPopUpChild.style.top = `${\n    \u002F\u002F   (window.innerHeight - container.offsetHeight - 100) \u002F 2\n    \u002F\u002F }px`; \u002F\u002F -100 offset for outerbase top bars\n\n    agPopUpChild.style.top = \"0px\"; \u002F\u002F Just hardcode at 0px otherwise top border not visible\n  }, 10);\n}\n",[14,1305,1306,1311,1316,1321,1325,1330,1335,1340,1345,1350,1355,1363,1367,1372,1377],{"__ignoreMap":141},[145,1307,1308],{"class":147,"line":148},[145,1309,1310],{},"setEditorPosition() {\n",[145,1312,1313],{"class":147,"line":155},[145,1314,1315],{},"  const agPopUpChild = document.querySelector(\".ag-popup-child\");\n",[145,1317,1318],{"class":147,"line":161},[145,1319,1320],{},"  const container = this.shadow.getElementById(\"container\");\n",[145,1322,1323],{"class":147,"line":240},[145,1324,268],{"emptyLinePlaceholder":267},[145,1326,1327],{"class":147,"line":246},[145,1328,1329],{},"  setTimeout(() => {\n",[145,1331,1332],{"class":147,"line":252},[145,1333,1334],{},"    agPopUpChild.style.left = `${\n",[145,1336,1337],{"class":147,"line":258},[145,1338,1339],{},"      (window.innerWidth - container.offsetWidth) \u002F 2\n",[145,1341,1342],{"class":147,"line":264},[145,1343,1344],{},"    }px`;\n",[145,1346,1347],{"class":147,"line":271},[145,1348,1349],{},"    \u002F\u002F agPopUpChild.style.top = `${\n",[145,1351,1352],{"class":147,"line":277},[145,1353,1354],{},"    \u002F\u002F   (window.innerHeight - container.offsetHeight - 100) \u002F 2\n",[145,1356,1357,1360],{"class":147,"line":283},[145,1358,1359],{},"    \u002F\u002F }px`;",[145,1361,1362],{}," \u002F\u002F -100 offset for outerbase top bars\n",[145,1364,1365],{"class":147,"line":288},[145,1366,268],{"emptyLinePlaceholder":267},[145,1368,1369],{"class":147,"line":294},[145,1370,1371],{},"    agPopUpChild.style.top = \"0px\"; \u002F\u002F Just hardcode at 0px otherwise top border not visible\n",[145,1373,1374],{"class":147,"line":300},[145,1375,1376],{},"  }, 10);\n",[145,1378,1379],{"class":147,"line":306},[145,1380,437],{},[63,1382,1383,1384,1387,1388],{},"The theme is set to the editor using ",[14,1385,1386],{},"theme: this.config.theme"," doesn't work without some manipulation. At the moment we do not receive the theme metadata in cell plugins. The below code finds the theme from the DOM styles",[136,1389,1391],{"className":218,"code":1390,"language":220,"meta":141,"style":141},"const agPopUp = document.querySelector(\".ag-popup\");\nconst colorScheme = window.getComputedStyle(agPopUp)[\"color-scheme\"];\nthis.config.theme = colorScheme === \"normal\" ? \"light\" : \"dark\";\n",[14,1392,1393,1398,1403],{"__ignoreMap":141},[145,1394,1395],{"class":147,"line":148},[145,1396,1397],{},"const agPopUp = document.querySelector(\".ag-popup\");\n",[145,1399,1400],{"class":147,"line":155},[145,1401,1402],{},"const colorScheme = window.getComputedStyle(agPopUp)[\"color-scheme\"];\n",[145,1404,1405],{"class":147,"line":161},[145,1406,1407],{},"this.config.theme = colorScheme === \"normal\" ? \"light\" : \"dark\";\n",[63,1409,1410,1411,1414,1415,1418,1419],{},"If we press enter in the markdown editor (to add a new line), the cell plugin editor closes itself (maybe there is a ",[14,1412,1413],{},"keydown"," event listener somewhere listening for ",[14,1416,1417],{},"Enter"," key events). We skirt through it by stopping such event propagation",[136,1420,1422],{"className":218,"code":1421,"language":220,"meta":141,"style":141},"events: { keydown: this.handleKeyDown }, \u002F\u002FListen for keydown events from the md editor\n\nhandleKeyDown(_, event) {\n  if (event.key === \"Enter\") {\n    event.stopPropagation();\n  }\n}\n",[14,1423,1424,1429,1433,1438,1443,1448,1452],{"__ignoreMap":141},[145,1425,1426],{"class":147,"line":148},[145,1427,1428],{},"events: { keydown: this.handleKeyDown }, \u002F\u002FListen for keydown events from the md editor\n",[145,1430,1431],{"class":147,"line":155},[145,1432,268],{"emptyLinePlaceholder":267},[145,1434,1435],{"class":147,"line":161},[145,1436,1437],{},"handleKeyDown(_, event) {\n",[145,1439,1440],{"class":147,"line":240},[145,1441,1442],{},"  if (event.key === \"Enter\") {\n",[145,1444,1445],{"class":147,"line":246},[145,1446,1447],{},"    event.stopPropagation();\n",[145,1449,1450],{"class":147,"line":252},[145,1451,255],{},[145,1453,1454],{"class":147,"line":258},[145,1455,437],{},[10,1457,1458],{},"Now we're ready to enjoy our writing with the md-editor. Once we're done, we can click the save button to close the editor and update the cell's content.",[136,1460,1462],{"className":218,"code":1461,"language":220,"meta":141,"style":141},"const saveBtn = this.shadow.getElementById(\"save-btn\");\nsaveBtn.addEventListener(\"click\", () => {\n  const finalContent = this.editor.getMarkdown();\n  triggerEvent_$PLUGIN_ID(this, {\n    action: OuterbaseColumnEvent_$PLUGIN_ID.onStopEdit,\n    value: finalContent,\n  });\n  triggerEvent_$PLUGIN_ID(this, {\n    action: OuterbaseColumnEvent_$PLUGIN_ID.updateCell,\n    value: finalContent,\n  });\n});\n",[14,1463,1464,1469,1474,1479,1484,1489,1494,1499,1503,1508,1512,1516],{"__ignoreMap":141},[145,1465,1466],{"class":147,"line":148},[145,1467,1468],{},"const saveBtn = this.shadow.getElementById(\"save-btn\");\n",[145,1470,1471],{"class":147,"line":155},[145,1472,1473],{},"saveBtn.addEventListener(\"click\", () => {\n",[145,1475,1476],{"class":147,"line":161},[145,1477,1478],{},"  const finalContent = this.editor.getMarkdown();\n",[145,1480,1481],{"class":147,"line":240},[145,1482,1483],{},"  triggerEvent_$PLUGIN_ID(this, {\n",[145,1485,1486],{"class":147,"line":246},[145,1487,1488],{},"    action: OuterbaseColumnEvent_$PLUGIN_ID.onStopEdit,\n",[145,1490,1491],{"class":147,"line":252},[145,1492,1493],{},"    value: finalContent,\n",[145,1495,1496],{"class":147,"line":258},[145,1497,1498],{},"  });\n",[145,1500,1501],{"class":147,"line":264},[145,1502,1483],{},[145,1504,1505],{"class":147,"line":271},[145,1506,1507],{},"    action: OuterbaseColumnEvent_$PLUGIN_ID.updateCell,\n",[145,1509,1510],{"class":147,"line":277},[145,1511,1493],{},[145,1513,1514],{"class":147,"line":283},[145,1515,1498],{},[145,1517,1518],{"class":147,"line":288},[145,1519,1520],{},"});\n",[87,1522,1524],{"id":1523},"chatgpt-the-secret-sauce-of-the-plugin","ChatGPT: The secret sauce of the plugin",[10,1526,1527],{},"This plugin comes integrated with ChatGPT to help with your writing with some preconfigured actions. Using it you can change the tone of your writing, get suggestions for headlines, generate a summary for your content, and more.",[10,1529,1530],{},"The below method prepares the UI for showing the tone selections.",[136,1532,1534],{"className":218,"code":1533,"language":220,"meta":141,"style":141},"prepareTonesSelections() {\n  const toneSelect = this.shadow.getElementById(\"tone-select\");\n  this.tones.forEach((value) => {\n    this.addSelectOption(toneSelect, value);\n  });\n\n  const toneBtn = this.shadow.getElementById(\"tone-btn\");\n  toneBtn.addEventListener(\"click\", () => {\n    const selectedIndex = toneSelect.selectedIndex;\n    if (selectedIndex) {\n      const selectedTone = toneSelect.options[selectedIndex].value;\n      const prompt = `Make the following text better and rewrite it in a ${selectedTone.toLowerCase()} tone`;\n      this.handleSelectionAction(prompt);\n    }\n  });\n}\n",[14,1535,1536,1541,1546,1551,1556,1560,1564,1569,1574,1579,1584,1589,1594,1599,1603,1607],{"__ignoreMap":141},[145,1537,1538],{"class":147,"line":148},[145,1539,1540],{},"prepareTonesSelections() {\n",[145,1542,1543],{"class":147,"line":155},[145,1544,1545],{},"  const toneSelect = this.shadow.getElementById(\"tone-select\");\n",[145,1547,1548],{"class":147,"line":161},[145,1549,1550],{},"  this.tones.forEach((value) => {\n",[145,1552,1553],{"class":147,"line":240},[145,1554,1555],{},"    this.addSelectOption(toneSelect, value);\n",[145,1557,1558],{"class":147,"line":246},[145,1559,1498],{},[145,1561,1562],{"class":147,"line":252},[145,1563,268],{"emptyLinePlaceholder":267},[145,1565,1566],{"class":147,"line":258},[145,1567,1568],{},"  const toneBtn = this.shadow.getElementById(\"tone-btn\");\n",[145,1570,1571],{"class":147,"line":264},[145,1572,1573],{},"  toneBtn.addEventListener(\"click\", () => {\n",[145,1575,1576],{"class":147,"line":271},[145,1577,1578],{},"    const selectedIndex = toneSelect.selectedIndex;\n",[145,1580,1581],{"class":147,"line":277},[145,1582,1583],{},"    if (selectedIndex) {\n",[145,1585,1586],{"class":147,"line":283},[145,1587,1588],{},"      const selectedTone = toneSelect.options[selectedIndex].value;\n",[145,1590,1591],{"class":147,"line":288},[145,1592,1593],{},"      const prompt = `Make the following text better and rewrite it in a ${selectedTone.toLowerCase()} tone`;\n",[145,1595,1596],{"class":147,"line":294},[145,1597,1598],{},"      this.handleSelectionAction(prompt);\n",[145,1600,1601],{"class":147,"line":300},[145,1602,947],{},[145,1604,1605],{"class":147,"line":306},[145,1606,1498],{},[145,1608,1609],{"class":147,"line":311},[145,1610,437],{},[10,1612,1613],{},"The action is handled in the below method where we go ahead only if some text is selected in the editor. We also show a \"Thinking...\" text as a loader and finally replace that with the result received from the API call.",[136,1615,1617],{"className":218,"code":1616,"language":220,"meta":141,"style":141},"async handleSelectionAction(prompt) {\n  const [start, end] = this.editor.getSelection();\n  const selectedText = this.editor.getSelectedText(start, end);\n  if (selectedText) {\n    this.editor.insertText(`${selectedText}\\n\\nThinking...`);\n    const currLinePos = end[0] + 2;\n    this.editor.setSelection(\n      [currLinePos, 1],\n      [currLinePos, \"Thinking...\".length + 1]\n    );\n\n    const generatedText = await this.talkToChatGPT(prompt, selectedText);\n    if (generatedText) {\n      this.editor.insertText(generatedText);\n    }\n  }\n}\n",[14,1618,1619,1624,1629,1634,1639,1644,1649,1654,1659,1664,1668,1672,1677,1682,1687,1691,1695],{"__ignoreMap":141},[145,1620,1621],{"class":147,"line":148},[145,1622,1623],{},"async handleSelectionAction(prompt) {\n",[145,1625,1626],{"class":147,"line":155},[145,1627,1628],{},"  const [start, end] = this.editor.getSelection();\n",[145,1630,1631],{"class":147,"line":161},[145,1632,1633],{},"  const selectedText = this.editor.getSelectedText(start, end);\n",[145,1635,1636],{"class":147,"line":240},[145,1637,1638],{},"  if (selectedText) {\n",[145,1640,1641],{"class":147,"line":246},[145,1642,1643],{},"    this.editor.insertText(`${selectedText}\\n\\nThinking...`);\n",[145,1645,1646],{"class":147,"line":252},[145,1647,1648],{},"    const currLinePos = end[0] + 2;\n",[145,1650,1651],{"class":147,"line":258},[145,1652,1653],{},"    this.editor.setSelection(\n",[145,1655,1656],{"class":147,"line":264},[145,1657,1658],{},"      [currLinePos, 1],\n",[145,1660,1661],{"class":147,"line":271},[145,1662,1663],{},"      [currLinePos, \"Thinking...\".length + 1]\n",[145,1665,1666],{"class":147,"line":277},[145,1667,365],{},[145,1669,1670],{"class":147,"line":283},[145,1671,268],{"emptyLinePlaceholder":267},[145,1673,1674],{"class":147,"line":288},[145,1675,1676],{},"    const generatedText = await this.talkToChatGPT(prompt, selectedText);\n",[145,1678,1679],{"class":147,"line":294},[145,1680,1681],{},"    if (generatedText) {\n",[145,1683,1684],{"class":147,"line":300},[145,1685,1686],{},"      this.editor.insertText(generatedText);\n",[145,1688,1689],{"class":147,"line":306},[145,1690,947],{},[145,1692,1693],{"class":147,"line":311},[145,1694,255],{},[145,1696,1697],{"class":147,"line":316},[145,1698,437],{},[10,1700,1701],{},"Below is the method which makes the API call to ChatGPT",[136,1703,1705],{"className":218,"code":1704,"language":220,"meta":141,"style":141},"async talkToChatGPT(instruction, text) {\n  const res = await fetch(\"https:\u002F\u002Fapi.openai.com\u002Fv1\u002Fcompletions\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application\u002Fjson\",\n      Authorization: `Bearer ${this.config.apiKey}`,\n    },\n    body: JSON.stringify({\n      model: \"gpt-3.5-turbo-instruct\",\n      prompt: `${instruction}: ${text}`,\n      max_tokens: 2048,\n      temperature: 0.3,\n      n: 1,\n    }),\n  });\n\n  const data = await res.json();\n  return data.choices[0].text.trim();\n}\n",[14,1706,1707,1712,1717,1722,1727,1732,1737,1742,1747,1752,1757,1762,1767,1772,1777,1781,1785,1789,1794],{"__ignoreMap":141},[145,1708,1709],{"class":147,"line":148},[145,1710,1711],{},"async talkToChatGPT(instruction, text) {\n",[145,1713,1714],{"class":147,"line":155},[145,1715,1716],{},"  const res = await fetch(\"https:\u002F\u002Fapi.openai.com\u002Fv1\u002Fcompletions\", {\n",[145,1718,1719],{"class":147,"line":161},[145,1720,1721],{},"    method: \"POST\",\n",[145,1723,1724],{"class":147,"line":240},[145,1725,1726],{},"    headers: {\n",[145,1728,1729],{"class":147,"line":246},[145,1730,1731],{},"      \"Content-Type\": \"application\u002Fjson\",\n",[145,1733,1734],{"class":147,"line":252},[145,1735,1736],{},"      Authorization: `Bearer ${this.config.apiKey}`,\n",[145,1738,1739],{"class":147,"line":258},[145,1740,1741],{},"    },\n",[145,1743,1744],{"class":147,"line":264},[145,1745,1746],{},"    body: JSON.stringify({\n",[145,1748,1749],{"class":147,"line":271},[145,1750,1751],{},"      model: \"gpt-3.5-turbo-instruct\",\n",[145,1753,1754],{"class":147,"line":277},[145,1755,1756],{},"      prompt: `${instruction}: ${text}`,\n",[145,1758,1759],{"class":147,"line":283},[145,1760,1761],{},"      max_tokens: 2048,\n",[145,1763,1764],{"class":147,"line":288},[145,1765,1766],{},"      temperature: 0.3,\n",[145,1768,1769],{"class":147,"line":294},[145,1770,1771],{},"      n: 1,\n",[145,1773,1774],{"class":147,"line":300},[145,1775,1776],{},"    }),\n",[145,1778,1779],{"class":147,"line":306},[145,1780,1498],{},[145,1782,1783],{"class":147,"line":311},[145,1784,268],{"emptyLinePlaceholder":267},[145,1786,1787],{"class":147,"line":316},[145,1788,678],{},[145,1790,1791],{"class":147,"line":322},[145,1792,1793],{},"  return data.choices[0].text.trim();\n",[145,1795,1796],{"class":147,"line":327},[145,1797,437],{},[10,1799,1800],{},"Similarly, we handle the other commands using other appropriate prompts. You can check the GitHub repo for the complete source code of the plugin.",[38,1802,1804],{"id":1803},"current-limitations","Current Limitations",[60,1806,1807,1810,1813],{},[63,1808,1809],{},"The cell plugin editors are closed automatically as soon as users click somewhere outside. It would be great to have an option to close the editor when users explicitly click on a button",[63,1811,1812],{},"The changed data does not persist even though correct events are sent to the parent DOM. One workaround is to use the Outerbase commands to make a call to the database, but due to lack of time I haven't explored that",[63,1814,1815],{},"The toolbar items that open a pop-up\u002Fdialog in the markdown editor (Heading \u002F Link \u002F Image etc.) do not work. The dialogs get closed as soon as you click on them. This is an open issue with the ToastUI editor where this functionality doesn't work in Shadow Dom. There is a workaround to replace these buttons with a similar button that doesn't open a dialog. Again due to lack of time, this has not been explored.",[38,1817,1819],{"id":1818},"resources","Resources",[10,1821,1822],{},"The complete source code of the plugins and the templates can be found here",[76,1824],{"url":1825},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fouterbase-adventures",[10,1827,1828],{},"The demo video showing the plugins in action",[76,1830],{"url":78},[38,1832,1834],{"id":1833},"conclusion","Conclusion",[10,1836,1837],{},"When you explore a new thing many roadblocks will come. Some roadblocks will have a workaround, and some will be dead ends. Now it is up to you whether you quit on the first roadblock, or push ahead and try to find a way. This Outerbase hackathon was one such adventure for me, and I thoroughly enjoyed it.",[10,1839,1840],{},"Hope you liked reading the article. Do let me know your thoughts in the comments section.",[10,1842,1843],{},[1844,1845,1846],"em",{},"Remember to keep adding the bits, soon you'll have more bytes than you'll ever need :-)",[10,1848,1849],{},"Until text time! Adios.",[1851,1852,1853],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":141,"searchDepth":155,"depth":155,"links":1855},[1856,1857,1860,1861,1862,1867,1868,1869],{"id":40,"depth":155,"text":41},{"id":81,"depth":155,"text":82,"children":1858},[1859],{"id":89,"depth":161,"text":90},{"id":468,"depth":155,"text":469},{"id":554,"depth":155,"text":555},{"id":726,"depth":155,"text":727,"children":1863},[1864,1865,1866],{"id":846,"depth":161,"text":847},{"id":1031,"depth":161,"text":1032},{"id":1523,"depth":161,"text":1524},{"id":1803,"depth":155,"text":1804},{"id":1818,"depth":155,"text":1819},{"id":1833,"depth":155,"text":1834},null,"\u002Fimages\u002Fposts\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt\u002F7a3213c083ad21700d87e2fbb2bb66b5-dbcb9b2815.jpeg","2023-10-02T03:28:27.792Z","This article is about exploring the new talk of the town, `bun`, getting to know `Outerbase`, and hanging out with old buddies `markdown` and `ChatGPT`. If you follow along, you...",false,"md","cln8bzn2o000209moajbn32z5",{},"\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt",{"title":5,"description":1873},"to-outerbase-with-bun-toastui-editor-and-chatgpt",[24,16,1882,1883,1884],"chatgpt","outerbasehackathon","outerbase","yojEhhN0hxy8jkOYmBRjW6fw7WbG8HBN69Qv2V9n72g",1780470201008]