diff --git a/.DS_Store b/.DS_Store index 8a26cb4..eee809d 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitmodules b/.gitmodules index cba1de2..551df18 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "themes/hugo-shortcode-roneo-collection"] path = themes/hugo-shortcode-roneo-collection url = https://gitlab.com/Roneo/hugo-shortcode-roneo-collection.git +[submodule "themes/PaperMod"] + path = themes/PaperMod + url = https://github.com/adityatelange/hugo-PaperMod.git +[submodule "themes/themes/hugo-shortcode-roneo-collection"] + path = themes/themes/hugo-shortcode-roneo-collection + url = https://gitlab.com/Roneo/hugo-shortcode-roneo-collection.git diff --git a/READ_FIRST.md b/READ_FIRST.md new file mode 100644 index 0000000..e8ea64d --- /dev/null +++ b/READ_FIRST.md @@ -0,0 +1,79 @@ +# Build Notes for Hugo + +## _ALL OF THESE COMMANDS SHOULD BE RUN FROM YOUR MAIN FOLDER - THE SAME ONE THAT CONTAINS THE HUGO.YAML_ + +TODO: Create script that loads submodules and theme after downloading git repo for first time. + +Upon first git clone AND if you want to build the submodules from scratch: + +```bash +git rm -rf --caches themes/ +git submodule add https://gitlab.com/Roneo/hugo-shortcode-roneo-collection.git themes/hugo-shortcode-roneo-collection +git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod +``` + +You can also try to run the git submodule update command, but this hasn't worked for me in the past. + +```bash +git submodule update --init --recursive # needed when you reclone your repo (submodules may not get cloned automatically) +``` + +For some Macs (for instance the 8GB M1), you have to run this to prevent the "too many open files" error: + +```bash +hugo check ulimit +sudo sysctl -w kern.maxfiles=65536 +sudo sysctl -w kern.maxfilesperproc=65536 +ulimit -n 65536 65536 +``` + +Run local: + +```bash +hugo server --baseURL 'localhost' --bind 'localhost' +``` + +And run local with drafts: + +```bash +hugo server --buildDrafts --baseURL 'localhost' --bind 'localhost' +``` + +Build to public folder ready for git push: + +```bash +hugo -s ./ -d ./public/ +``` + +## CONTENT + +Create content with the most used settings +FEAT: Shouldn't this just become the default one instead of having a more specific command? +```bash +hugo new content --kind page posts/name_of_post.md +``` + +## DESIGN + +When using the shortcodes from Roneo for Box and/or the detail (drop down/accordion style menu), the formatting can clash +with the theme. So you'll want to remove box shadow, border, background, padding, etc. Here's the basic setup I insert at the +end of each MD post that needs these changes. +```html + + +``` diff --git a/content/posts/google_scripts_sheets_to_slides.md b/content/posts/google_scripts_sheets_to_slides.md new file mode 100644 index 0000000..7fae51b --- /dev/null +++ b/content/posts/google_scripts_sheets_to_slides.md @@ -0,0 +1,456 @@ +--- +title: 'Google Sheets to Slides with Scripts: an Automation' +date: 2024-02-25T10:14:30-05:00 +tags: ["google", "scripts", "sheets"] +categories: ["Tutorial"] +author: "Me" +showToc: true +TocOpen: false +draft: true +hidemeta: false +description: "Learn about running a quick automation that turns rows in your Google Sheets and plugs it into a Google [Slide](2024-02-26_slide.md) template to easily share more attractive content." +disableHLJS: true # to disable highlightjs +disableShare: false +disableHLJS: false +hideSummary: false +searchHidden: true +ShowReadingTime: true +ShowBreadCrumbs: true +ShowPostNavLinks: true +ShowWordCount: true +ShowRssButtonInSectionTermList: true +UseHugoToc: true +cover: + image: "" + alt: "" + caption: "" + relative: false + hidden: true +--- +# Overview + +Recently, my wife needed help in sharing weekly content with a group of people. The original way this group was sharing +content was a PDF export of a Google Doc. From a User Experience perspective, it wasn't great. Someone would received this +long PDF, they would have to scroll to find the date or topic of the next additional_notes, and overall didn't look great. + +While she had developed the Google Sheets and Google Slide system to make everything a bit more legible and navigable, there +was still an element of copy and paste from the Sheet to the Slide. She had asked if I knew of any ways to make this process +easier. This was a perfect mini-project for Google Apps Scripts! + +{{< box info >}} +In case you haven't heard of Google Apps Scripts, it is a built in Google IDE that uses Javascript to make various +Google products available via automation and scripting. While Google saves the files as `.gs` files, it's just javascript, +don't worry! You can learn more about it [here](https://www.google.com/script/start/) and [here](https://developers.google.com/apps-script/overview). +{{< /box >}} + +Below, I'll share the pieces of the script to explain what is going on. If you know what you're doing and just want the +script, feel free to head to the bottom of the page to see the [full script]({{}}). + +# Goal + +The goal here was pretty straight forward. I wanted to add a button on the Google Sheet that allows the user to create a +Slides presentation from a subset of data within the sheet. The data will be date based, as this is what most end users need, +and the user will be able to pick from the currently available dates & populated data. + +# Setup + +Getting setup, you need both a Sheet with a few headings and an empty Slide presentation with a few empty text boxes. The +empty text boxes will be key to helping us connect the data in the sheet to the correct placement in the slide. Here are two +screenshots of what this looks like for my example: + +![Google Sheets Example](../posts/img/google_sheets_for_scripts_example.png) + +![Google Slides Example](../posts/img/google_slides_for_scripts_example.png) + +## Sheets Setup + +Ignoring any design from my screenshots - all credit goes to my much more creative wife - the setup for the sheet is fairly +simple. You need various headings in Row A of the sheet which we will be using to reference data. In this tutorial, our +headings are `Date, Topic 1, Topic 2, Topic 3, Additional Notes`. Whether you start on Row 1 or after that doesn't matter too +much. + +For the date column, we'll be formatting our date like this: "February 26, 2024". You'll see why in a little bit. + +On top of the actual data in the Sheet, the Apps Script is going to live in this document and just push data to the Slide. To +access your App Scripts, click Extensions > Apps Scripts. A new tab will open with a blank IDE style interface and an empty +`myFunction`. + +## Slides Setup + +Don't worry too much about the design for Slides, you can change that later. But the important step is creating the empty +text boxes. After you create your text boxes (4 will be used in this tutorial), right click one of them and select "Format +Options". A panel on the left hand-side should slide out. Click the "Alt Text" drop down, and then "Advanced Options". That +little text box is the title for your text box; it is not used in the visual representation of the box, we will just be using +it as a reference point. + +For ease of this tutorial, make the Title of the text box the same as the Header row from when we set up the Sheet, above. +Once you've added title to each of the text boxes, let's head into the code. + +## onOpen Function + +The first function you need for creating a UI change in the Google Sheet is an `onOpen` function that will setup the UI when +the Sheet is open. + +```js {linenos=true} +function onOpen(e) { + SpreadsheetApp.getUi() + .createMenu('Create Presentation') + .addItem('Create Presentation', 'askDate' ) + .addToUi(); +} +``` + +In this function (which we'll later trigger in the script settings), when someone opens up the Sheet, it will create the UI +button in the top level menu. Here's what's happening: + +* `SpreadsheetApp.getUi()` --> Instantiate class and get available methods for `getUi()`. +* `createMenu()` --> Creates a menu item called "Create Presentation". +* `addItem()` --> Adds an item to that menu that when pressed, calls the `askDate` function. +* `addtoUi()` --> Add it! Now people can see and click on it. + +![Google Sheets Menu Example](../posts/img/google_sheets_menu_example_2.png) + +## askDate Function + +This is the main function and a bit long, so I'll split it up into a few sections. + +### Section 1 + +```js {linenos=true} + var ui = SpreadsheetApp.getUi(); + var sheet = SpreadsheetApp.getActiveSheet(); + var lastRow = sheet.getLastRow(); + var dates = sheet.getRange(2,2,lastRow,1).getDisplayValues(); + + // get today's epoch time for easier calculations + const epochDate = new Date().getTime() +``` + +This first section we're just getting the sheet ready for analysis. With the `dates` variable, we're just looking at the +second column. The reason we're using the `getDisplayValues()` method is because Google will automatically convert the dates +to include time zone, time, etc. We want to keep the date in the same format for a better user experience. + +For that last line, we're getting the current date & time in epoch time so we can run a comparison further down the script. +For this use case, we don't need to include any dates in the past. + +### Section 2 + +```javascript {linenos=true} + menuOptions = [] + const re = new RegExp("^[A-Za-z]{3,15}\\s\\d{1,2},\\s\\d{2,4}") + for (let i = 0; i < dates.length; i++ ) { + var date = dates[i][0]; + var epochSheetDate = new Date(date).getTime(); + if (date != "") { + var dateMatch = re.test(date) + if (dateMatch == true) { + if (epochDate < epochSheetDate) { + menuOptions.push(date) + } + } + } + } +``` + +In this section we're creating an empty array and instantiating a RegExp to ensure we have an actual date in the cell. See +[Regex101](https://regex101.com) to learn more about Regex and test different regex syntax. + +{{< box tip >}} +Something that threw me off when first writing this regex function was the way the Google IDE manages escape character and +slashes. If you take the second line above and input it into [Regex101](https://regex101.com) you'll see the `\s` or `\d` +become dark gray, basically skipping over that token. However, for Google, you'll need an additional backslash to escape and +make the token become used by the function. + +Here's the "correct" RegExp string for Regex101.com: `^[A-Za-z]{3,15}\s\d{1,2},\s\d{2,4}` +{{< /box >}} + +Next, we dive into a for loop, looping through the `dates` column of values (which we just called earlier). After attributing +each value to the `var date` variable, we also convert that same value into epoch time (by creating a new `Date().getTime()`) +so that we can compare it with today's date. + +After the variables are setup we need to check that the date isn't empty; we don't need any rows where a date hasn't been assigned to it +yet. If we have a non-empty date value, let's compare it using the regex string. All we're doing here is asking "Is this date +in the format I'm expecting it?" If true, let's keep the value and continue using it. If not, just ignore it. + +So we've now found a value that's in the date format we expect, let's now take that same value in epoch time (referenced by +the `epochSheetDate` variable) and compare it to today's epoch time date. If today's date is less than the value in the +sheet, that means the date in the sheet is in the future. + +So now we have a date in the correct format and that is at some future date from today. Fantastic! Once we've gone through +those checks, we're ready to add the date to the array we created at the top of this section. Push on! + +### Section 3 + +```javascript {linenos=true} +if (menuOptions.length > 0) { + let stringList = menuOptions.toString(); + var formattedDates = stringList.replaceAll("2024,", "2024 \n "); + var response = ui.prompt('Which date would you like to create a presentation for? Please copy and paste the date exactly as you see it in the options below. Options: \n \n'+formattedDates); + if (response.getSelectedButton() == ui.Button.OK) { + var dateChosen = response.getResponseText(); + if (dateChosen != "") { + ui.alert("Woohoo! Let's make a presentation!") + for(var y = 0; y}} +A `ui.alert` is just a modal that offers no interaction to the user. +{{< /box >}} + +The next few lines are for processing that array of dates we grabbed from the sheet to making them presentable to the user. +Without the `stringList` and `formattedDates` variables, Google's modal just shows a wall of text of dates which makes it +difficult to parse for the user. By using `replaceAll()` we remove the comma and insert a new line after every portion of the +string that contains 2024 and a comma. + +So now, instead of the modal showing: + +``` +February 2, 2024,February 3, 2024,February 4,2024... etc +``` + +We now see the much easier to read: + +``` +February 2, 2024 +February 3, 2024 +February 4, 2024 +``` + +After that's formatted nicely, we can now have the prompt show for the user which asks them what date they would like to +process into a Slide presentation. Since we haven't visited any ui options since the top of this tutorial, this is the modal +that will show up after a user clicks "Create Presentation" in the top level menu in Google Sheets. + +Ok, so now moving onto line 5, we need to get the text for what the user enters into the modal box. If they don't enter +anything, we should let them know that nothing will be done. That message comes from the second to last `else` statement in +this section. Instead, if they enter some data let's parse it! + +{{< box warning >}} +If you want to test yourself by adding in some new code try this exercise: + +_How can we ensure that the user entered a date in the format we expect? Tip: It is something reusable from another portion +of the script._ +{{< /box >}} + +For lines 9 and 10, we're now looping back through the same set of rows that we did in section 2. The reason we're +re-entering this for loop is because we've already exited the loop. So we have to get the values again. Once we have them, +let's match the date that the user entered to the correct row. We're able to do that by grabbing the index and adding 2 to +it. + +{{< box info >}} +I haven't fully looked into _why_ we need to add 2 to get the correct row number. As we loop through the dates, the array +length (`dates.length`) is all the available rows in the sheet. So you would think that `y` is the row that the value is on; +but it's not. + +My only guess so far has to do with the frozen row at the top that I have in my Sheet. So the frozen row is not used, and row +2 is now index 0. Which means index 41 is technically row 43, and why we have to add 2 to the value to get the _visually +correct_ row number. We need this number to be correct so that we can get the rest of the values int he sheet. +{{< /box >}} + +Now that we have the correct row number, we're going to shift from getting values from each row and getting them from each +column. By calling `var presData = sheet.getRange(presRow,3,1,4).getValues();` we are grabbing an array. `getRange` is +looking for the following values, in the following order: + +* Row number (which we got from the `y+2`) +* Column Number (which is Column C in this case) +* Number of Rows (we only need this row) +* Number of Columns (we know how many more columns we need). + +Next up, we're jumping into another for loop, now for all the data we just pulled from the column. This part is just parsing +each column's data into it's own variable for easier management when we create the presentation. We've also grabbed the +additional notes column, but we're not using it right now. This column's data may be valuable by adding it to the speaker +notes of the Slide Presentation. Check out these two resources: + +* [Working with Speaker Notes](https://developers.google.com/slides/api/guides/notes) +* [Notes Properties API](https://developers.google.com/slides/api/reference/rest/v1/presentations.pages#Page.NotesProperties) + +Once that's done, call the next function: `createPresentation()`! We're almost done. + +## createPresentation Function + +```js {linenos=true} +function createPresentation(dateChosen, topic1, topic2, topic3, additionalNotes) { + const slides = SlidesApp.openById('ENTER_ID_OF_SLIDE_PRESENTATION'); + let slide = slides.getSlides()[0]; + let elements = slide.getPageElements(); + for (let i = 0; i < elements.length; i++) { + var titles = elements[i].getTitle() + if (titles == "Date") { + var dateText = elements[i].asShape().getText(); + dateText.clear(); + dateText.setText(dateChosen); + } if (titles == "Topic 1") { + const topic1Text = elements[i].asShape().getText(); + topic1Text.clear(); + topic1Text.setText(topic1); + } if (titles == "Topic 2") { + const topic2Text = elements[i].asShape().getText(); + topic2Text.clear(); + topic2Text.setText(topic2); + } if (titles == "Topic 3") { + const topic3Text = elements[i].asShape().getText(); + topic3Text.clear(); + topic3Text.setText(topic3); + } + } +} +``` + +This function may be the shortest to explain. All we are doing is taking those variables from the previous function (the +variables that contain each topic's text value) and doing the following: + +1. Opening the Presentation (line 2) +2. Getting the first slide (line 3) +3. Grabbing all the [page elements](https://developers.google.com/apps-script/reference/slides/slides/page-element) (line 4) +4. Looping through the page elements & grabbing all the titles that we set during [setup]({{}}) (line + 5 & 6) +5. Then we compare the title of the Slide text box to the expected string (Topic 1-3), and if we get a match, retrieve the text + of the box as a variable, clear the text, and input our new text with the variables we created in the previous function. + +And just like that, you should have all your boxes filled in with the data you input to the Google Sheet! + +{{< box tip >}} +**Bonus exercises:** + +* Instead of overwriting this presentation each time, how can you add a new slide with the same text boxes? How would you +find that slide since we're right now grabbing the first slide? +* How can you change the name of this Slide Presentation to the date from the Sheet? +* Could you automatically export this Slide as a PDF and save it to a user's Drive? +{{< /box >}} + +{{< details "## Full Script:" >}} + +```js {linenos=true} +function onOpen(e) { + SpreadsheetApp.getUi() + .createMenu('Create Presentation') + .addItem('Create Presentation', 'askDate' ) + .addToUi(); +} + +function askDate() { + var ui = SpreadsheetApp.getUi(); + var sheet = SpreadsheetApp.getActiveSheet(); + var lastRow = sheet.getLastRow(); + var dates = sheet.getRange(2,2,lastRow,1).getDisplayValues(); + + // get today's epoch time for easier calculations + const epochDate = new Date().getTime() + + menuOptions = [] + const re = new RegExp("^[A-Za-z]{3,15}\\s\\d{1,2},\\s\\d{2,4}") + for (let i = 0; i < dates.length; i++ ) { + var date = dates[i][0]; + var epochSheetDate = new Date(date).getTime(); + if (date != "") { + var dateMatch = re.test(date) + if (dateMatch == true) { + if (epochDate < epochSheetDate) { + menuOptions.push(date) + } + } + } + } + + if (menuOptions.length > 0) { + let stringList = menuOptions.toString(); + var stringlist = stringList.replaceAll("2024,", "2024 \n "); + var response = ui.prompt('Which date would you like to create a presentation for? Please copy and paste the date exactly as you see it in the options below. Options: \n \n'+stringlist); + if (response.getSelectedButton() == ui.Button.OK) { + var dateChosen = response.getResponseText(); + if (dateChosen != "") { + ui.alert("Woohoo! Let's make a presentation!") + for(var y = 0; y}} + + diff --git a/content/posts/img/google_sheets_for_scripts_example.png b/content/posts/img/google_sheets_for_scripts_example.png new file mode 100644 index 0000000..358e455 Binary files /dev/null and b/content/posts/img/google_sheets_for_scripts_example.png differ diff --git a/content/posts/img/google_sheets_menu_example.png b/content/posts/img/google_sheets_menu_example.png new file mode 100644 index 0000000..8ce267c Binary files /dev/null and b/content/posts/img/google_sheets_menu_example.png differ diff --git a/content/posts/img/google_sheets_menu_example_2.png b/content/posts/img/google_sheets_menu_example_2.png new file mode 100644 index 0000000..611eb31 Binary files /dev/null and b/content/posts/img/google_sheets_menu_example_2.png differ diff --git a/content/posts/img/google_slides_for_scripts_example.png b/content/posts/img/google_slides_for_scripts_example.png new file mode 100644 index 0000000..6b0f60d Binary files /dev/null and b/content/posts/img/google_slides_for_scripts_example.png differ diff --git a/hugo.yaml b/hugo.yaml index 6e3ab29..0390e89 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -4,7 +4,9 @@ relativeURLs: true languageCode: 'en-us' title: "Norm-working Packets \U0001F4BE" subtitle: 'A semi-technical blog & series of experiences working in tech and my homelab' -theme: "PaperMod" +theme: + - hugo-shortcode-roneo-collection + - PaperMod themesDir: './themes/' paginate: 7 @@ -19,6 +21,10 @@ taxonomies: # Allow html to be rendered in Markdown files markup: + highlight: + lineNoStart: 1 + lineNos: true + style: vim goldmark: renderer: unsafe: true @@ -39,6 +45,7 @@ params: defaultTheme: dark ShowShareButtons: true ShowBreadCrumbs: true + ShowCodeCopyButtons: true assets: favicon: "favicon.ico" favicon32x32: "rsmsncircles.ico" diff --git a/public/404.html b/public/404.html index 169d69c..28f7ad2 100644 --- a/public/404.html +++ b/public/404.html @@ -10,7 +10,7 @@ - + @@ -18,8 +18,7 @@ - - + + + + + + + + + + + + + + + +
+ +
+
+ +
+
+

Tutorial: Move from NginxProxyManager to Nginx +

+
+
+

Goal A Tutorial Repo for migrating your Nginx Proxy Manager proxy setup to Nginx. I wrote this originally for this reddit post and to post this my Github profile. Thought my website would also be a good place to share it for any passers-by. +To give clear instructions to help users migrate from using Nginx Proxy Manager (NPM) to standard Nginx. This tutorial is not exhaustive and there are many other implementations of this transition....

+
+
August 5, 2023 ยท 8 min ยท 1635 words ยท Me
+ +
+ +
+
+

My First Merged PR! +

+
+
+

Admittedly, I feel a bit like a child sharing something like this, as there are so many devs that pull and merge requests from contributors on a regular basis. However, while Iโ€™ve contributed to documentation and/or tutorials and other non-coding portions of repositories, I feel a tiny bit proud that this was the first instance where I was using a library, found a bug, created an issue, cloned the repo to my local machine, found and fixed the code, and opened a pull request....

+
+
September 1, 2022 ยท 2 min ยท 328 words ยท Me
+ +
+ +
+ + + + + + + + + + + + + + diff --git a/public/posts.html b/public/posts.html index 2b6d9ca..ba0d7c9 100644 --- a/public/posts.html +++ b/public/posts.html @@ -10,7 +10,7 @@ - + @@ -19,8 +19,7 @@ - - + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+
+ +

+ Google Sheets to Slides with Scripts: an Automation + + + + + +

+
+ Learn about running a quick automation that turns rows in your Google Sheets and plugs it into a Google [Slide](2024-02-26_slide.md) template to easily share more attractive content. +
+ +
+ +

Overview

+

Recently, my wife needed help in sharing weekly content with a group of people. The original way this group was sharing +content was a PDF export of a Google Doc. From a User Experience perspective, it wasn’t great. Someone would received this +long PDF, they would have to scroll to find the date or topic of the next additional_notes, and overall didn’t look great.

+

While she had developed the Google Sheets and Google Slide system to make everything a bit more legible and navigable, there +was still an element of copy and paste from the Sheet to the Slide. She had asked if I knew of any ways to make this process +easier. This was a perfect mini-project for Google Apps Scripts!

+ + + + + + + + + + + + + + + + + + +
+ + + +

In case you haven’t heard of Google Apps Scripts, it is a built in Google IDE that uses Javascript to make various +Google products available via automation and scripting. While Google saves the files as .gs files, it’s just javascript, +don’t worry! You can learn more about it here and here.

+
+ +

Below, I’ll share the pieces of the script to explain what is going on. If you know what you’re doing and just want the +script, feel free to head to the bottom of the page to see the full script.

+

Goal

+

The goal here was pretty straight forward. I wanted to add a button on the Google Sheet that allows the user to create a +Slides presentation from a subset of data within the sheet. The data will be date based, as this is what most end users need, +and the user will be able to pick from the currently available dates & populated data.

+

Setup

+

Getting setup, you need both a Sheet with a few headings and an empty Slide presentation with a few empty text boxes. The +empty text boxes will be key to helping us connect the data in the sheet to the correct placement in the slide. Here are two +screenshots of what this looks like for my example:

+

Google Sheets Example +

+

Google Slides Example +

+

Sheets Setup

+

Ignoring any design from my screenshots - all credit goes to my much more creative wife - the setup for the sheet is fairly +simple. You need various headings in Row A of the sheet which we will be using to reference data. In this tutorial, our +headings are Date, Topic 1, Topic 2, Topic 3, Additional Notes. Whether you start on Row 1 or after that doesn’t matter too +much.

+

For the date column, we’ll be formatting our date like this: “February 26, 2024”. You’ll see why in a little bit.

+

On top of the actual data in the Sheet, the Apps Script is going to live in this document and just push data to the Slide. To +access your App Scripts, click Extensions > Apps Scripts. A new tab will open with a blank IDE style interface and an empty +myFunction.

+

Slides Setup

+

Don’t worry too much about the design for Slides, you can change that later. But the important step is creating the empty +text boxes. After you create your text boxes (4 will be used in this tutorial), right click one of them and select “Format +Options”. A panel on the left hand-side should slide out. Click the “Alt Text” drop down, and then “Advanced Options”. That +little text box is the title for your text box; it is not used in the visual representation of the box, we will just be using +it as a reference point.

+

For ease of this tutorial, make the Title of the text box the same as the Header row from when we set up the Sheet, above. +Once you’ve added title to each of the text boxes, let’s head into the code.

+

onOpen Function

+

The first function you need for creating a UI change in the Google Sheet is an onOpen function that will setup the UI when +the Sheet is open.

+
+ +
+
1
+2
+3
+4
+5
+6
+
+
function onOpen(e) {
+    SpreadsheetApp.getUi()
+      .createMenu('Create Presentation')
+      .addItem('Create Presentation', 'askDate' )
+      .addToUi();
+}
+
+
+

In this function (which we’ll later trigger in the script settings), when someone opens up the Sheet, it will create the UI +button in the top level menu. Here’s what’s happening:

+
    +
  • SpreadsheetApp.getUi() –> Instantiate class and get available methods for getUi().
  • +
  • createMenu() –> Creates a menu item called “Create Presentation”.
  • +
  • addItem() –> Adds an item to that menu that when pressed, calls the askDate function.
  • +
  • addtoUi() –> Add it! Now people can see and click on it.
  • +
+

Google Sheets Menu Example +

+

askDate Function

+

This is the main function and a bit long, so I’ll split it up into a few sections.

+

Section 1

+
+ +
+
1
+2
+3
+4
+5
+6
+7
+
+
  var ui = SpreadsheetApp.getUi();
+  var sheet = SpreadsheetApp.getActiveSheet();
+  var lastRow = sheet.getLastRow();
+  var dates = sheet.getRange(2,2,lastRow,1).getDisplayValues();
+
+  // get today's epoch time for easier calculations
+  const epochDate = new Date().getTime()
+
+
+

This first section we’re just getting the sheet ready for analysis. With the dates variable, we’re just looking at the +second column. The reason we’re using the getDisplayValues() method is because Google will automatically convert the dates +to include time zone, time, etc. We want to keep the date in the same format for a better user experience.

+

For that last line, we’re getting the current date & time in epoch time so we can run a comparison further down the script. +For this use case, we don’t need to include any dates in the past.

+

Section 2

+
+ +
+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+
+
  menuOptions = []
+  const re = new RegExp("^[A-Za-z]{3,15}\\s\\d{1,2},\\s\\d{2,4}")
+  for (let i = 0; i < dates.length; i++ ) {
+    var date = dates[i][0];
+    var epochSheetDate = new Date(date).getTime();
+    if (date != "") {
+      var dateMatch = re.test(date)
+      if (dateMatch == true) {
+        if (epochDate < epochSheetDate) {
+          menuOptions.push(date)
+        }
+      }
+    }
+  }
+
+
+

In this section we’re creating an empty array and instantiating a RegExp to ensure we have an actual date in the cell. See +Regex101 to learn more about Regex and test different regex syntax.

+ + +
+ + + +

Something that threw me off when first writing this regex function was the way the Google IDE manages escape character and +slashes. If you take the second line above and input it into Regex101 you’ll see the \s or \d +become dark gray, basically skipping over that token. However, for Google, you’ll need an additional backslash to escape and +make the token become used by the function.

+

Here’s the “correct” RegExp string for Regex101.com: ^[A-Za-z]{3,15}\s\d{1,2},\s\d{2,4}

+
+ +

Next, we dive into a for loop, looping through the dates column of values (which we just called earlier). After attributing +each value to the var date variable, we also convert that same value into epoch time (by creating a new Date().getTime()) +so that we can compare it with today’s date.

+

After the variables are setup we need to check that the date isn’t empty; we don’t need any rows where a date hasn’t been assigned to it +yet. If we have a non-empty date value, let’s compare it using the regex string. All we’re doing here is asking “Is this date +in the format I’m expecting it?” If true, let’s keep the value and continue using it. If not, just ignore it.

+

So we’ve now found a value that’s in the date format we expect, let’s now take that same value in epoch time (referenced by +the epochSheetDate variable) and compare it to today’s epoch time date. If today’s date is less than the value in the +sheet, that means the date in the sheet is in the future.

+

So now we have a date in the correct format and that is at some future date from today. Fantastic! Once we’ve gone through +those checks, we’re ready to add the date to the array we created at the top of this section. Push on!

+

Section 3

+
+ +
+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+
+
if (menuOptions.length > 0) {
+  let stringList = menuOptions.toString();
+  var formattedDates = stringList.replaceAll("2024,", "2024 \n ");
+  var response = ui.prompt('Which date would you like to create a presentation for? Please copy and paste the date exactly as you see it in the options below. Options: \n \n'+formattedDates);
+  if (response.getSelectedButton() == ui.Button.OK) {
+    var dateChosen = response.getResponseText();
+    if (dateChosen != "") {
+      ui.alert("Woohoo! Let's make a presentation!")
+      for(var y = 0; y<dates.length;y++){
+        if (dates[y][0] == dateChosen) {
+          // Logger.log("Row:"+(y+2));
+          var presRow = y+2
+          var presData = sheet.getRange(presRow,3,1,4).getValues();
+          for (presCopy of presData) {
+            var topic1 = presCopy[0];
+            var topic2 = presCopy[1];
+            var topic3 = presCopy[2];
+            var additional_notes = presCopy[3];
+          }
+          createPresentation(dateChosen, topic1, topic2, topic3, additional_notes)
+        }
+      }
+    } else {
+      ui.alert("Ouch. Looks like you entered something incorrectly (i.e. you entered nothing). Try again.")
+    }
+  }
+} else {
+  ui.alert("No Dates Available. Check Spreadsheet")
+}
+
+
+

This next section might feel long, but it’s really easy to follow, don’t worry. It’s a lot of data organization to ensure +we’re giving the users a smooth experience & to make sure when the data hits the Slide, it’s in the right spot.

+

First things first - let’s only run this if we actually have items in the menuOptions array! No need to give the user an +affirmative message when there’s no data to process. If there are no dates available, we call the else of this if, which is +the 3rd to last line in this section.

+ + +
+ + + +

A ui.alert is just a modal that offers no interaction to the user.

+
+ +

The next few lines are for processing that array of dates we grabbed from the sheet to making them presentable to the user. +Without the stringList and formattedDates variables, Google’s modal just shows a wall of text of dates which makes it +difficult to parse for the user. By using replaceAll() we remove the comma and insert a new line after every portion of the +string that contains 2024 and a comma.

+

So now, instead of the modal showing:

+
February 2, 2024,February 3, 2024,February 4,2024... etc
+

We now see the much easier to read:

+
February 2, 2024
+February 3, 2024
+February 4, 2024
+

After that’s formatted nicely, we can now have the prompt show for the user which asks them what date they would like to +process into a Slide presentation. Since we haven’t visited any ui options since the top of this tutorial, this is the modal +that will show up after a user clicks “Create Presentation” in the top level menu in Google Sheets.

+

Ok, so now moving onto line 5, we need to get the text for what the user enters into the modal box. If they don’t enter +anything, we should let them know that nothing will be done. That message comes from the second to last else statement in +this section. Instead, if they enter some data let’s parse it!

+ + +
+ + + +

If you want to test yourself by adding in some new code try this exercise:

+

How can we ensure that the user entered a date in the format we expect? Tip: It is something reusable from another portion +of the script.

+
+ +

For lines 9 and 10, we’re now looping back through the same set of rows that we did in section 2. The reason we’re +re-entering this for loop is because we’ve already exited the loop. So we have to get the values again. Once we have them, +let’s match the date that the user entered to the correct row. We’re able to do that by grabbing the index and adding 2 to +it.

+ + +
+ + + +

I haven’t fully looked into why we need to add 2 to get the correct row number. As we loop through the dates, the array +length (dates.length) is all the available rows in the sheet. So you would think that y is the row that the value is on; +but it’s not.

+

My only guess so far has to do with the frozen row at the top that I have in my Sheet. So the frozen row is not used, and row +2 is now index 0. Which means index 41 is technically row 43, and why we have to add 2 to the value to get the visually +correct row number. We need this number to be correct so that we can get the rest of the values int he sheet.

+
+ +

Now that we have the correct row number, we’re going to shift from getting values from each row and getting them from each +column. By calling var presData = sheet.getRange(presRow,3,1,4).getValues(); we are grabbing an array. getRange is +looking for the following values, in the following order:

+
    +
  • Row number (which we got from the y+2)
  • +
  • Column Number (which is Column C in this case)
  • +
  • Number of Rows (we only need this row)
  • +
  • Number of Columns (we know how many more columns we need).
  • +
+

Next up, we’re jumping into another for loop, now for all the data we just pulled from the column. This part is just parsing +each column’s data into it’s own variable for easier management when we create the presentation. We’ve also grabbed the +additional notes column, but we’re not using it right now. This column’s data may be valuable by adding it to the speaker +notes of the Slide Presentation. Check out these two resources:

+ +

Once that’s done, call the next function: createPresentation()! We’re almost done.

+

createPresentation Function

+
+ +
+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+
+
function createPresentation(dateChosen, topic1, topic2, topic3, additionalNotes) {
+    const slides = SlidesApp.openById('ENTER_ID_OF_SLIDE_PRESENTATION');
+    let slide = slides.getSlides()[0];
+    let elements = slide.getPageElements();
+    for (let i = 0; i < elements.length; i++) {
+      var titles = elements[i].getTitle()
+      if (titles == "Date") {
+        var dateText = elements[i].asShape().getText();
+        dateText.clear();
+        dateText.setText(dateChosen);
+      } if (titles == "Topic 1") {
+        const topic1Text = elements[i].asShape().getText();
+        topic1Text.clear();
+        topic1Text.setText(topic1);
+      } if (titles == "Topic 2") {
+        const topic2Text = elements[i].asShape().getText();
+        topic2Text.clear();
+        topic2Text.setText(topic2);
+      } if (titles == "Topic 3") {
+        const topic3Text = elements[i].asShape().getText();
+        topic3Text.clear();
+        topic3Text.setText(topic3);
+      }
+    }
+}
+
+
+

This function may be the shortest to explain. All we are doing is taking those variables from the previous function (the +variables that contain each topic’s text value) and doing the following:

+
    +
  1. Opening the Presentation (line 2)
  2. +
  3. Getting the first slide (line 3)
  4. +
  5. Grabbing all the page elements (line 4)
  6. +
  7. Looping through the page elements & grabbing all the titles that we set during setup (line +5 & 6)
  8. +
  9. Then we compare the title of the Slide text box to the expected string (Topic 1-3), and if we get a match, retrieve the text +of the box as a variable, clear the text, and input our new text with the variables we created in the previous function.
  10. +
+

And just like that, you should have all your boxes filled in with the data you input to the Google Sheet!

+ + +
+ + + +

Bonus exercises:

+
    +
  • Instead of overwriting this presentation each time, how can you add a new slide with the same text boxes? How would you +find that slide since we’re right now grabbing the first slide?
  • +
  • How can you change the name of this Slide Presentation to the date from the Sheet?
  • +
  • Could you automatically export this Slide as a PDF and save it to a user’s Drive?
  • +
+
+ + + +

Full Script:

+
+
+ +
+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+
+
function onOpen(e) {
+    SpreadsheetApp.getUi()
+      .createMenu('Create Presentation')
+      .addItem('Create Presentation', 'askDate' )
+      .addToUi();
+}
+
+function askDate() {
+  var ui = SpreadsheetApp.getUi();
+  var sheet = SpreadsheetApp.getActiveSheet();
+  var lastRow = sheet.getLastRow();
+  var dates = sheet.getRange(2,2,lastRow,1).getDisplayValues();
+
+  // get today's epoch time for easier calculations
+  const epochDate = new Date().getTime()
+
+  menuOptions = []
+  const re = new RegExp("^[A-Za-z]{3,15}\\s\\d{1,2},\\s\\d{2,4}")
+  for (let i = 0; i < dates.length; i++ ) {
+    var date = dates[i][0];
+    var epochSheetDate = new Date(date).getTime();
+    if (date != "") {
+      var dateMatch = re.test(date)
+      if (dateMatch == true) {
+        if (epochDate < epochSheetDate) {
+          menuOptions.push(date)
+        }
+      }
+    }
+  }
+
+  if (menuOptions.length > 0) {
+    let stringList = menuOptions.toString();
+    var stringlist = stringList.replaceAll("2024,", "2024 \n ");
+    var response = ui.prompt('Which date would you like to create a presentation for? Please copy and paste the date exactly as you see it in the options below. Options: \n \n'+stringlist);
+    if (response.getSelectedButton() == ui.Button.OK) {
+      var dateChosen = response.getResponseText();
+      if (dateChosen != "") {
+        ui.alert("Woohoo! Let's make a presentation!")
+        for(var y = 0; y<dates.length;y++){
+          if (dates[y][0] == dateChosen) {
+            // Logger.log("Row:"+(y+2));
+            var presRow = y+2
+            var presData = sheet.getRange(presRow,3,1,4).getValues();
+            for (presCopy of presData) {
+              var topic1 = presCopy[0];
+              var topic2 = presCopy[1];
+              var topic3 = presCopy[2];
+              var additional_notes = presCopy[3];
+            }
+            createPresentation(dateChosen, topic1, topic2, topic3, additionalNotes)
+          }
+      }
+    } else {
+        ui.alert("Ouch. Looks like you entered something incorrectly (i.e. you entered nothing). Try again.")
+      }
+  }
+} else {
+    ui.alert("No Dates Available. Check Spreadsheet")
+  }
+}
+
+function createPresentation(dateChosen, topic1, topic2, topic3, additionalNotes) {
+    const slides = SlidesApp.openById('ENTER_ID_OF_SLIDE_PRESENTATION');
+    let slide = slides.getSlides()[0];
+    let elements = slide.getPageElements();
+    for (let i = 0; i < elements.length; i++) {
+      var titles = elements[i].getTitle()
+      if (titles == "Date") {
+        var dateText = elements[i].asShape().getText();
+        dateText.clear();
+        dateText.setText(dateChosen);
+      } if (titles == "topic1") {
+        const topic1Text = elements[i].asShape().getText();
+        topic1Text.clear();
+        topic1Text.setText(topic1);
+      } if (titles == "topic2") {
+        const topic2Text = elements[i].asShape().getText();
+        topic2Text.clear();
+        topic2Text.setText(topic2);
+      } if (titles == "topic3") {
+        const topic3Text = elements[i].asShape().getText();
+        topic3Text.clear();
+        topic3Text.setText(topic3);
+      }
+    }
+}
+
+
+
+
+ + + + +
+ + +
+
+ + + + + + + + + + + + + + + diff --git a/public/posts/hosting_hugo_troubles.html b/public/posts/hosting_hugo_troubles.html index 721567b..6956d78 100644 --- a/public/posts/hosting_hugo_troubles.html +++ b/public/posts/hosting_hugo_troubles.html @@ -10,9 +10,7 @@ - - + @@ -20,8 +18,7 @@ - - + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+
+ +

+ Neovim: A Config Debugging Tale + + + + + +

+
+ If you have used Neovim and it's massive plugin ecosystem for any length of time, then you're aware that debugging just comes with the territory. Here's my tale. Warning: It hasn't been solved at the time of writing. +
+ +
+ + + +
+
+ + + + + + + + + + + + + + + diff --git a/public/posts/new-favorite-website.html b/public/posts/new-favorite-website.html index 44719f7..411f24d 100644 --- a/public/posts/new-favorite-website.html +++ b/public/posts/new-favorite-website.html @@ -1,7 +1,7 @@ - + @@ -9,17 +9,16 @@ - - - - - - - - + + + + + + + + - - - + - - + + - + @@ -58,11 +46,17 @@ if (!doNotTrack) { "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ + { + "@type": "ListItem", + "position": 1 , + "name": "Posts", + "item": "//localhost:1313/posts.html" + } { "@type": "ListItem", "position": 1 , "name": "New Favorite Website!", - "item": "/posts/new-favorite-website.html" + "item": "//localhost:1313/posts/new-favorite-website.html" } ] } @@ -80,7 +74,7 @@ if (!doNotTrack) { "articleBody": "For the longest time, I had Regex101 as a bookmarked website for (almost) daily use. Not only does it help me build muscle memory for using regex queries without banging my head against the wall with a bunch of print and debug statements, but if I happen to be in a different language - say in javascript instead of python - it gives you the correct syntax for using regex in that language. There are even spin-off projects to use this tool while offline.\nYou can even test it again the strings you do (or donโ€™t!) want to verify against. This website is beyond cool.\nSo this morning, when I saw Jรฉrรฉmy Garniaux ask if there was an โ€œexplain shellโ€ for Vim or Neovim, this was the Regex101 but for shell commands.\nSide Note: it took me way too long how to figure out how to embed an iframe directly into Hugo without creating a new shortcode template and other suggestions. Iโ€™ll be sure to post about that in the future.\nWhen you first load ExplainShell.com, youโ€™re greeted with a clean, minimal interface with some explanation and a very obvious search bar.\nFor those new to the command line, it also shares some suggested queries to search with. As you read over the results, you can hover on the different elements of the command and it will highlight the explanation below. I mean, look at how clean and nice that is!\nAs many other command line users are apt to do, I use the man page all the time for commands and flags Iโ€™m unsure of, or need a refresher on. While knowing what a certain flag is and what it does for a specific command is supremely helpful, I find the man pages a tad bit overwhelming. You can always grep for what youโ€™re looking for, but even then Iโ€™ve found times where it only pulls out half of the full description, or even just the line the definition is on.\nSo the fact that this resources can extract exactly what you need in your command from a man page without digging through every line, is extremely useful! Going back to Jรฉrรฉmyโ€™s original toot and questionโ€ฆ who is going to make this same tool for Neovim?\n", "wordCount" : "377", "inLanguage": "en", - "image":"/new-website-cover.png","datePublished": "2023-09-27T10:07:01-04:00", + "image":"//localhost:1313/new-website-cover.png","datePublished": "2023-09-27T10:07:01-04:00", "dateModified": "2023-09-27T10:07:01-04:00", "author":{ "@type": "Person", @@ -88,14 +82,14 @@ if (!doNotTrack) { }, "mainEntityOfPage": { "@type": "WebPage", - "@id": "/posts/new-favorite-website.html" + "@id": "//localhost:1313/posts/new-favorite-website.html" }, "publisher": { "@type": "Organization", "name": "Norm-working Packets ๐Ÿ’พ", "logo": { "@type": "ImageObject", - "url": "/favicon.ico" + "url": "//localhost:1313/favicon.ico" } } } @@ -113,7 +107,7 @@ if (!doNotTrack) {
@@ -145,9 +146,15 @@ if (!doNotTrack) {
- -

+ +

New Favorite Website! + + + + +

Regex101 has long been one of my favorite reference tools. As of today, I will be adding ExplainShell to the list of must-use tools!. @@ -298,24 +305,97 @@ Neovim?