diff --git a/jgclark.DailyJournal/plugin.json b/jgclark.DailyJournal/plugin.json index 33ee3b3d9..c5bcece49 100644 --- a/jgclark.DailyJournal/plugin.json +++ b/jgclark.DailyJournal/plugin.json @@ -6,14 +6,14 @@ "plugin.icon": "", "plugin.author": "Jonathan Clark", "plugin.url": "https://github.com/NotePlan/plugins/blob/main/jgclark.DailyJournal/README.md", - "plugin.version": "0.6.2", + "plugin.version": "0.6.4", "plugin.dependencies": [], "plugin.script": "script.js", "plugin.isRemote": "false", "plugin.commands": [ { "name": "dayStart", - "description": "Start today's daily note with a template, including local weather lookup (requires configuring)", + "description": "Start today's daily note with a template (requires configuring)", "jsFunction": "dayStart", "requested_interval": "24h" }, diff --git a/jgclark.DailyJournal/script.js b/jgclark.DailyJournal/script.js index c9dfe9cd5..cd7e56bba 100644 --- a/jgclark.DailyJournal/script.js +++ b/jgclark.DailyJournal/script.js @@ -1,19 +1,6 @@ var exports = (function (exports) { 'use strict'; - async function chooseOption(title, options, defaultValue) { - const { - index - } = await CommandBar.showOptions(options.map(option => option.label), title); - return options[index]?.value ?? defaultValue; - } - async function showMessage(title, okLabel = 'OK') { - await CommandBar.showOptions([okLabel], title); - } - async function getInput(title, okLabel = 'OK') { - return await CommandBar.showInput(title, okLabel); - } - var parser$1 = function () { /* * Generated by PEG.js 0.8.0. @@ -9235,6 +9222,19 @@ var exports = (function (exports) { }; var load = loader.load; + async function chooseOption(title, options, defaultValue) { + const { + index + } = await CommandBar.showOptions(options.map(option => option.label), title); + return options[index]?.value ?? defaultValue; + } + async function showMessage(title, okLabel = 'OK') { + await CommandBar.showOptions([okLabel], title); + } + async function getInput(title, okLabel = 'OK') { + return await CommandBar.showInput(title, okLabel); + } + const staticTemplateFolder = '📋 Templates'; function getTemplateFolder() { return DataStore.folders.find(f => f.includes(staticTemplateFolder)); @@ -9332,6 +9332,19 @@ The first code-block within the note will always be used. So edit the default co timeStyle: 'short', }, + // configuration for weather data + weather: { + // API key for https://openweathermap.org/ + // !!REQUIRED!! + openWeatherAPIKey: '... put your API key here ...', + // Default location for weather forcast + latPosition: 0.0, + longPosition: 0.0, + // Default units. Can be 'metric' (for Celsius), or 'metric' (for Fahrenheit) + openWeatherUnits: 'metric', + // When using a weather tag, you can customize these options. + }, + // default values for custom tags. // These tags cannot be functions, but you may choose to have nested objects. // feel free to edit this value however you see fit. @@ -9364,6 +9377,18 @@ date: # can be null (to skip time), "short", "medium", "long" or "full" timeStyle: short +# configuration for weather data lookups, if wanted +weather: + # API key for https://openweathermap.org/ + # !!REQUIRED!! + openWeatherAPIKey: + # Default location for weather forcast + latPosition: 0.0 + longPosition: 0.0 + # Default units. Can be 'metric' (for Celsius), or 'metric' (for Fahrenheit) + openWeatherUnits: metric + # When using a weather tag, you can customize these options. + # default values for custom tags. # These tags cannot be functions, but you may choose to have nested objects. # feel free to edit this value however you see fit. @@ -9392,6 +9417,18 @@ dateStyle = "short" # can be null (to skip time), "short", "medium", "long" or "full" timeStyle = "short" +// configuration for weather data +[weather] +// API key for https://openweathermap.org/ +# !!REQUIRED!! +openWeatherAPIKey = +# Default location for weather forcast +latPosition = 0.0 +longPosition = 0.0 +# Default units. Can be 'metric' (for Celsius), or 'metric' (for Fahrenheit) +openWeatherUnits = 'metric' +# When using a weather tag, you can customize these options. + # default values for custom tags. [tagValue] # These tags cannot be functions, but you may choose to have nested objects. @@ -9404,24 +9441,6 @@ firstName = "John" lastName = "Doe" \`\`\` `; - /** - * - * The following should be added to the default configuration - * once the weather function works. - * - // configuration for weather data - weather: { - // API key for https://openweathermap.org/ - // !!REQUIRED!! - apiKey: '... put your API key here ...', - // Default location for weather forcast - lattitude: 0, - longitude: 0, - // Default temperature unit. Can be "C" (Celcius), "K" (Kelvin) or "F" (Fahrenheit) - unit: 'C', - // When using a weather tag, you can customize these options. - }, - */ const ALLOWED_FORMATS = ['javascript', 'json', 'json5', 'yaml', 'toml', 'ini']; const FORMAT_MAP = { @@ -9545,14 +9564,11 @@ lastName = "Doe" const pref_openWeatherAPIKey = weatherConfig.openWeatherAPIKey; const pref_latPosition = weatherConfig.latPosition; const pref_longPosition = weatherConfig.longPosition; - const pref_openWeatherUnits = weatherConfig.openWeatherUnits; // TODO: probably getDefaultConfiguration rather than parseJSON5 ? - + const pref_openWeatherUnits = weatherConfig.openWeatherUnits; console.log(`getWeatherSummary: Params: '${weatherParams}'`); - const paramConfig = weatherParams.trim() ? await parseJSON5(weatherParams) : {}; - console.log(paramConfig); - const getWeatherURL = `https://api.openweathermap.org/data/2.5/onecall?lat=${pref_latPosition}&lon=${pref_longPosition}&exclude=current,hourly,minutely&units=${pref_openWeatherUnits}&appid=${pref_openWeatherAPIKey}`; // TODO: Allow for more customisation of what is pulled out from the API's data structure - // using weatherParams + weatherParams.trim() ? await parseJSON5(weatherParams) : {}; // console.log(paramConfig) + const getWeatherURL = `https://api.openweathermap.org/data/2.5/onecall?lat=${pref_latPosition}&lon=${pref_longPosition}&exclude=current,hourly,minutely&units=${pref_openWeatherUnits}&appid=${pref_openWeatherAPIKey}`; const jsonIn = await fetch(getWeatherURL); if (jsonIn != null) { @@ -9570,18 +9586,21 @@ lastName = "Doe" weatherIcon = weatherDescIcons[i]; break; } - } + } // TODO: Allow for more customisation of what is pulled out from the API's data structure + // using weatherParams + - const summaryLine = `Weather: ${maxTemp}/${minTemp} ${weatherIcon}${weatherDesc}`; + const summaryLine = `${maxTemp}/${minTemp} ${weatherIcon}${weatherDesc}`; console.log(`\t${summaryLine}`); return summaryLine; } else { await showMessage('Sorry; error in Weather lookup'); return 'sorry; error in Weather lookup'; } - } // globalThis.getWeatherSummary = getWeatherSummary + } async function processTemplate(content, config) { + console.log(`processTemplate: ${content}`); const tagStart = content.indexOf('{{'); const tagEnd = content.indexOf('}}'); const hasTag = tagStart !== -1 && tagEnd !== -1 && tagStart < tagEnd; @@ -9602,21 +9621,11 @@ lastName = "Doe" console.log(e); return content; } - } // Apply any matching tag values, asking user for value if not found in configuration - - async function processTagValues(tag, config) { - const valueInConfig = tag // eslint-disable-next-line no-useless-escape - .split(/[\.\[\]]/).filter(Boolean).reduce((path, key) => path != null && typeof path === 'object' ? path[key] : null, config.tagValue); - - if (valueInConfig != null) { - return String(valueInConfig); - } - - return await getInput(`Value for ${tag}`); } // Apply any matching tag functions - async function processTags(tag, config) { + console.log(`processTag: ${tag}`); + if (tag.startsWith('date(') && tag.endsWith(')')) { return await processDate(tag.slice(5, tag.length - 1), config); } else if (tag.startsWith('weather(') && tag.endsWith(')')) { @@ -9629,10 +9638,23 @@ lastName = "Doe" // no matching funcs, so now attempt to match defined tag values instead return processTagValues(tag, config); } + } // Apply any matching tag values, asking user for value if not found in configuration + + async function processTagValues(tag, config) { + const valueInConfig = tag // eslint-disable-next-line no-useless-escape + .split(/[\.\[\]]/).filter(Boolean).reduce((path, key) => path != null && typeof path === 'object' ? path[key] : null, config.tagValue); + + if (valueInConfig != null) { + return String(valueInConfig); + } + + return await getInput(`Value for ${tag}`); } // ---------------------------------------------------------------- // Define new tag functions here ... + async function processDate(dateParams, config) { + console.log(`processDate: ${dateConfig}`); const defaultConfig = config.date ?? {}; const paramConfig = dateParams.trim() ? await parseJSON5(dateParams) : {}; // console.log(`param config: ${dateParams} as ${JSON.stringify(paramConfig)}`); @@ -9679,80 +9701,54 @@ lastName = "Doe" } //-------------------------------------------------------------------------------------------------------------------- - // Daily Journal plugin for NotePlan - // Jonathan Clark - // v0.6.0, 8.6.2021 - //-------------------------------------------------------------------------------------------------------------------- - // Title of template note to use as Daily template - // const staticTemplateFolder = '📋 Templates'; - const pref_templateTitle = 'Daily Note Template'; // Settings that should come from the Preference framework in time: - - const pref_reviewSectionHeading = 'Journal'; - const pref_reviewQuestions = ['@work()', '@fruitveg()', 'Exercise:: ', 'Mood:: ', 'Gratitude:: ', 'God was:: ', 'Alive:: ', 'Not Great:: ', 'Wife:: ', 'Remember:: '].join('\n'); - const pref_moods = ['🤩 Great', '🙂 Good', '😇 Blessed', '🥱 Tired', '😫 Stressed', '😤 Frustrated', '😡 Angry', '😔 Low', '🥵 Sick', 'Other'].join(','); // Globals - const todaysDate = new Date().toISOString().slice(0, 10).replace(/-/g, ''); - const pref_moodArray = pref_moods.split(','); // with a proper config system, this won't be needed + const pref_templateTitle = 'Daily Note Template'; + const todaysDate = new Date().toISOString().slice(0, 10).replace(/-/g, ''); //------------------------------------------------------------------ + // Helper functions // test for integer // taken from https://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript function isInt(value) { const x = parseFloat(value); return !isNaN(value) && (x | 0) === x; - } // function templateFolder() { - // return DataStore.folders.find((f) => f.includes(staticTemplateFolder)); - // } - // async function getTemplateContent(templateTitle) { - // const folder = templateFolder(); - // if (folder == null) { - // // template folder not found - // console.log("Error: Failed to find the '📋 Templates' folder"); - // return; - // } - // // Get list of templates from its folder - // const templateNotes = DataStore.projectNotes.filter((n) => - // n.filename.includes(folder), - // ); - // const templateNote = templateNotes.find((note) => note.title === templateTitle); - // // Now cut out everything above "---" (second line), which is there so we can have a more meaningful title for the template note - // if (templateNote != null) { - // const lines = [...templateNote.paragraphs]; - // if (lines.length > 0 && lines[1].content == '---') { - // lines.splice(1, 1); - // lines.splice(0, 1); - // } - // return lines.map((l) => l.rawContent).join('\n'); - // } else { - // console.log(`Error: Failed to get template note '${ templateTitle }' from the index`); - // } - // } - //------------------------------------------------------------------ + } //------------------------------------------------------------------ // Start today's daily note with a template, including local weather lookup if configured async function dayStart() { console.log(`\ndayStart for ${todaysDate}`); // open today's date in the main window, and read content - await Editor.openNoteByDate(new Date(), false); // get daily template's content - NB: deprecating this in favour of newer nmn.Templates plugin - // const templateText = await getTemplateContent(pref_templateTitle); - // console.log(`\tRead template text from Template '${ pref_templateTitle }'`); - // let newContent = `${templateText }\n`; + await Editor.openNoteByDate(new Date(), false); // apply daily template, using @nmn Template system - await applyNamedTemplate(pref_templateTitle); // Now add the weather, if wanted - // if (pref_openWeatherAPIKey != "") { - // const weatherLine = await getWeatherSummary() - // Editor.insertParagraph(`${weatherLine }\n`, 0, 'empty') - // } + await applyNamedTemplate(pref_templateTitle); } //------------------------------------------------------------------ // Gather answers to set questions, and append to the daily note - // TODO: use NP API function calls which are now available. - // TODO: can this use nmn.Template functions at all? async function dayReview() { - console.log(`\ndailyReview for ${todaysDate}`); - Editor.openNoteByDate(new Date()); // open today's date in main window + console.log(`\ndailyReview for ${todaysDate}`); // Get config settings from Template folder _configuration note + + const config = (await getDefaultConfiguration()) ?? {}; + const journalConfig = config.dailyJournal ?? null; - const questionsString = pref_reviewQuestions; // Plugin.preference.review_questions + if (journalConfig == null) { + console.log("\tWarning: Cannot find 'journal' settings in Templates/_configuration note. Stopping."); + await showMessage("Cannot find 'journal' settings in Templates/_configuration note"); + return; + } + + const pref_reviewQuestions = journalConfig.reviewQuestions ?? null; + + if (pref_reviewQuestions == null) { + console.log("\tWarning: Cannot find any 'reviewQuestions' setting in Templates/_configuration note. Stopping."); + await showMessage("Cannot find any 'reviewQuestions' setting in Templates/_configuration note"); + return; + } + + const pref_reviewSectionHeading = journalConfig.reviewSectionHeading ?? "Journal"; + const pref_moods = journalConfig.moods ?? ['🤩 Great', '🙂 Good', '😇 Blessed', '🥱 Tired', '😫 Stressed', '😤 Frustrated', '😡 Angry', '😔 Low', '🥵 Sick', 'Other'].join(','); + const pref_moodArray = pref_moods.split(','); // with a proper config system, this won't be needed + + Editor.openNoteByDate(new Date()); // open today's date in main window const question = []; const questionType = []; @@ -9760,14 +9756,14 @@ lastName = "Doe" let i = 0; // Parse preference string to make array of questions and input types const typeRE = new RegExp('<(.*)>'); - const questionLine = questionsString.split('\n'); - const numQs = questionLine.length; + const questionLines = pref_reviewQuestions.split('\n'); + const numQs = questionLines.length; console.log(`\tFound ${numQs} question lines`); for (i = 0; i < numQs; i++) { // remove type indicators from the question string - question[i] = questionLine[i].replace(/:|\(|\)|||/g, '').trim(); - const reArray = questionLine[i].match(typeRE); + question[i] = questionLines[i].replace(/:|\(|\)|||/g, '').trim(); + const reArray = questionLines[i].match(typeRE); questionType[i] = reArray[1]; // console.log("\t" + i + ": " + question[i] + " / " + questionType[i]) } // Ask each question in turn @@ -9783,9 +9779,9 @@ lastName = "Doe" { const reply = await CommandBar.showInput(questionType[i], `${question[i]}: %@`); - if (reply != undefined && isInt(reply)) { + if (reply != null && isInt(reply)) { // console.log(reply) - reviewLine = questionLine[i].replace(//, reply); + reviewLine = questionLines[i].replace(//, reply); } else { console.log(`\tERROR trying to get integer answer for question '${question[i]}'`); } @@ -9797,9 +9793,9 @@ lastName = "Doe" { const replyString = await CommandBar.showInput(questionType[i], `${question[i]}: %@`); - if (replyString != undefined) { + if (replyString != null) { // console.log(replyString) - reviewLine = replyString != '' ? questionLine[i].replace(//, replyString) : ''; + reviewLine = replyString !== '' ? questionLines[i].replace(//, replyString) : ''; } else { console.log(`\tERROR trying to get string answer for question '${question[i]}'`); } @@ -9812,9 +9808,9 @@ lastName = "Doe" const reply = await CommandBar.showOptions(pref_moodArray, 'Choose appropriate mood'); const replyMood = pref_moodArray[reply.index]; - if (replyMood != undefined && replyMood != '') { + if (replyMood != null && replyMood !== '') { // console.log(replyMood) - reviewLine = questionLine[i].replace(//, replyMood); + reviewLine = questionLines[i].replace(//, replyMood); } else { console.log('\tERROR trying to get mood answer'); } @@ -9824,7 +9820,7 @@ lastName = "Doe" } // console.log("\tAnswer to '" + question[i] + "' = " + reviewLine[i]) - if (reviewLine != '') { + if (reviewLine !== '') { output += `${reviewLine}\n`; } } // add the finished review text to the current daily note, @@ -9832,15 +9828,12 @@ lastName = "Doe" // If this doesn't exist, then append it first. - console.log(`Appending to heading '${pref_reviewSectionHeading}' the text:${output}`); + console.log(`\tAppending to heading '${pref_reviewSectionHeading}' the text:${output}`); Editor.note.addParagraphBelowHeadingTitle(output, '', pref_reviewSectionHeading, true, true); } - globalThis.dayStart = dayStart; - globalThis.dayReview = dayReview; exports.dayReview = dayReview; exports.dayStart = dayStart; - exports.getWeatherSummary = getWeatherSummary; Object.defineProperty(exports, '__esModule', { value: true }); diff --git a/jgclark.DailyJournal/src/index.js b/jgclark.DailyJournal/src/index.js index 0e672fdbf..a529ed46f 100644 --- a/jgclark.DailyJournal/src/index.js +++ b/jgclark.DailyJournal/src/index.js @@ -1,11 +1,9 @@ // @flow //----------------------------------------------------------------------------- -// Statistic commands for notes and projects -// Jonathan Clark & Eduard Metzger -// v0.6.0, 8.6.2021 +// Daily Journal commands +// Jonathan Clark +// v0.6.4, 18.6.2021 //----------------------------------------------------------------------------- -// export { default as showNoteCount } from './showNoteCount' export { dayStart, dayReview } from './script' -export { getWeatherSummary } from './weather' diff --git a/jgclark.DailyJournal/src/script.js b/jgclark.DailyJournal/src/script.js index 90c198266..d5950ac8f 100644 --- a/jgclark.DailyJournal/src/script.js +++ b/jgclark.DailyJournal/src/script.js @@ -171,6 +171,3 @@ export async function dayReview() { true, ) } - -// globalThis.dayStart = dayStart -// globalThis.dayReview = dayReview diff --git a/nmn.Templates/plugin.json b/nmn.Templates/plugin.json index e57070627..915dcc94b 100644 --- a/nmn.Templates/plugin.json +++ b/nmn.Templates/plugin.json @@ -6,7 +6,7 @@ "plugin.icon": "f120", "plugin.author": "Naman Goel", "plugin.repoUrl": "tbd", - "plugin.version": "0.0.1", + "plugin.version": "0.1.0", "plugin.dependencies": [ ], "plugin.script": "script.js", "plugin.isRemote": "false", @@ -14,7 +14,7 @@ { "name": "it", "description": "Choose a Template to apply to an existing Note", - "jsFunction": "addTemplate" + "jsFunction": "applyTemplate" }, { "name": "nn", diff --git a/nmn.Templates/script.js b/nmn.Templates/script.js index 2ad7af218..96424d195 100644 --- a/nmn.Templates/script.js +++ b/nmn.Templates/script.js @@ -9329,7 +9329,20 @@ The first code-block within the note will always be used. So edit the default co // can be "short", "medium", "long" or "full" dateStyle: 'short', // optional key, can be "short", "medium", "long" or "full" - timeStyle: 'short, + timeStyle: 'short', + }, + + // configuration for weather data + weather: { + // API key for https://openweathermap.org/ + // !!REQUIRED!! + openWeatherAPIKey: '... put your API key here ...', + // Default location for weather forcast + latPosition: 0.0, + longPosition: 0.0, + // Default units. Can be 'metric' (for Celsius), or 'metric' (for Fahrenheit) + openWeatherUnits: 'metric', + // When using a weather tag, you can customize these options. }, // default values for custom tags. @@ -9364,6 +9377,18 @@ date: # can be null (to skip time), "short", "medium", "long" or "full" timeStyle: short +# configuration for weather data lookups, if wanted +weather: + # API key for https://openweathermap.org/ + # !!REQUIRED!! + openWeatherAPIKey: + # Default location for weather forcast + latPosition: 0.0 + longPosition: 0.0 + # Default units. Can be 'metric' (for Celsius), or 'metric' (for Fahrenheit) + openWeatherUnits: metric + # When using a weather tag, you can customize these options. + # default values for custom tags. # These tags cannot be functions, but you may choose to have nested objects. # feel free to edit this value however you see fit. @@ -9392,6 +9417,18 @@ dateStyle = "short" # can be null (to skip time), "short", "medium", "long" or "full" timeStyle = "short" +// configuration for weather data +[weather] +// API key for https://openweathermap.org/ +# !!REQUIRED!! +openWeatherAPIKey = +# Default location for weather forcast +latPosition = 0.0 +longPosition = 0.0 +# Default units. Can be 'metric' (for Celsius), or 'metric' (for Fahrenheit) +openWeatherUnits = 'metric' +# When using a weather tag, you can customize these options. + # default values for custom tags. [tagValue] # These tags cannot be functions, but you may choose to have nested objects. @@ -9404,24 +9441,6 @@ firstName = "John" lastName = "Doe" \`\`\` `; - /** - * - * The following should be added to the default configuration - * once the weather function works. - * - // configuration for weather data - weather: { - // API key for https://openweathermap.org/ - // !!REQUIRED!! - apiKey: '... put your API key here ...', - // Default location for weather forcast - lattitude: 0, - longitude: 0, - // Default temperature unit. Can be "C" (Celcius), "K" (Kelvin) or "F" (Fahrenheit) - unit: 'C', - // When using a weather tag, you can customize these options. - }, - */ const ALLOWED_FORMATS = ['javascript', 'json', 'json5', 'yaml', 'toml', 'ini']; const FORMAT_MAP = { @@ -9529,7 +9548,59 @@ lastName = "Doe" } } + // Using https://openweathermap.org/api/one-call-api#data, for which you can get a free API key + + async function getWeatherSummary(weatherParams, config) { + const weatherDescText = ['showers', 'rain', 'sunny intervals', 'partly sunny', 'sunny', 'cloud', 'snow ', 'thunderstorm', 'tornado']; + const weatherDescIcons = ["🌦️", "🌧️", "🌤", "⛅", "☀️", "☁️", "🌨️", "⛈", "🌪"]; // Get config settings from Template folder _configuration note + + const weatherConfig = config.weather ?? null; + + if (weatherConfig == null) { + await showMessage("Cannot find 'weather' settings in Templates/_configuration note"); + return ''; + } + + const pref_openWeatherAPIKey = weatherConfig.openWeatherAPIKey; + const pref_latPosition = weatherConfig.latPosition; + const pref_longPosition = weatherConfig.longPosition; + const pref_openWeatherUnits = weatherConfig.openWeatherUnits; + console.log(`getWeatherSummary: Params: '${weatherParams}'`); + weatherParams.trim() ? await parseJSON5(weatherParams) : {}; // console.log(paramConfig) + + const getWeatherURL = `https://api.openweathermap.org/data/2.5/onecall?lat=${pref_latPosition}&lon=${pref_longPosition}&exclude=current,hourly,minutely&units=${pref_openWeatherUnits}&appid=${pref_openWeatherAPIKey}`; + const jsonIn = await fetch(getWeatherURL); + + if (jsonIn != null) { + const weatherTodayAll = JSON.parse(jsonIn).daily["0"]; + const maxTemp = weatherTodayAll.feels_like.day.toFixed(0); + const minTemp = weatherTodayAll.feels_like.night.toFixed(0); + const weatherDesc = weatherTodayAll.weather["0"].description; // see if we can fix an icon for this as well, according to returned description. Main terms are: + // thunderstorm, drizzle, shower > rain, snow, sleet, clear sky, mist, fog, dust, tornado, overcast > clouds + // with 'light' modifier for rain and snow + + let weatherIcon = ''; + + for (let i = 0; i < weatherDescText.length; i++) { + if (weatherDesc.match(weatherDescText[i])) { + weatherIcon = weatherDescIcons[i]; + break; + } + } // TODO: Allow for more customisation of what is pulled out from the API's data structure + // using weatherParams + + + const summaryLine = `${maxTemp}/${minTemp} ${weatherIcon}${weatherDesc}`; + console.log(`\t${summaryLine}`); + return summaryLine; + } else { + await showMessage('Sorry; error in Weather lookup'); + return 'sorry; error in Weather lookup'; + } + } + async function processTemplate(content, config) { + console.log(`processTemplate: ${content}`); const tagStart = content.indexOf('{{'); const tagEnd = content.indexOf('}}'); const hasTag = tagStart !== -1 && tagEnd !== -1 && tagStart < tagEnd; @@ -9543,20 +9614,33 @@ lastName = "Doe" const tag = content.slice(tagStart + 2, tagEnd); try { - const tagProcessed = await processTag(tag, config); + const tagProcessed = await processTags(tag, config); const restProcessed = await processTemplate(afterTag, config); return beforeTag + tagProcessed + restProcessed; } catch (e) { console.log(e); return content; } - } + } // Apply any matching tag functions + + async function processTags(tag, config) { + console.log(`processTag: ${tag}`); - async function processTag(tag, config) { if (tag.startsWith('date(') && tag.endsWith(')')) { return await processDate(tag.slice(5, tag.length - 1), config); - } - + } else if (tag.startsWith('weather(') && tag.endsWith(')')) { + return await getWeatherSummary(tag.slice(8, tag.length - 1), config); + } // **Add other extension function calls here** + // Can call functions defined in other plugins, by appropriate use + // of imports at top of file (e.g. getWeatherSummary) + // Or declare below (e.g. processDate) + else { + // no matching funcs, so now attempt to match defined tag values instead + return processTagValues(tag, config); + } + } // Apply any matching tag values, asking user for value if not found in configuration + + async function processTagValues(tag, config) { const valueInConfig = tag // eslint-disable-next-line no-useless-escape .split(/[\.\[\]]/).filter(Boolean).reduce((path, key) => path != null && typeof path === 'object' ? path[key] : null, config.tagValue); @@ -9565,15 +9649,19 @@ lastName = "Doe" } return await getInput(`Value for ${tag}`); - } + } // ---------------------------------------------------------------- + // Define new tag functions here ... - async function processDate(dateConfig, config) { + + async function processDate(dateParams, config) { + console.log(`processDate: ${dateConfig}`); const defaultConfig = config.date ?? {}; - const paramConfig = dateConfig.trim() ? await parseJSON5(dateConfig) : {}; // console.log(`param config: ${dateConfig} as ${JSON.stringify(paramConfig)}`); + const paramConfig = dateParams.trim() ? await parseJSON5(dateParams) : {}; // console.log(`param config: ${dateParams} as ${JSON.stringify(paramConfig)}`); const finalArguments = { ...defaultConfig, ...paramConfig - }; + }; // ... = "gather the remaining parameters into an array" + const { locale, ...otherParams @@ -9587,11 +9675,35 @@ lastName = "Doe" return new Intl.DateTimeFormat(localeParam, secondParam).format(new Date()); } - async function addTemplate(newNote) { + async function applyNamedTemplate(templateName) { + console.log(`applyNamedTemplate: for template '${templateName}'`); + const templateFolder = await getTemplateFolder(); + + if (templateFolder == null) { + console.log(`\twarning: templateFolder is null`); + await makeTemplateFolder(); + await showMessage('Try using this command again to use a template'); + return; + } + + const selectedTemplate = DataStore.projectNoteByTitle(templateName, true, false)[0]; + let templateContent = selectedTemplate?.content; + + if (templateContent == null || templateContent.length === 0) { + console.log(`\twarning: template '${templateName}' is null or empty`); + return; + } + + templateContent = templateContent.split('\n---\n').slice(1).join('\n---\n'); + const config = (await getDefaultConfiguration()) ?? {}; + const processedTemplateContent = await processTemplate(templateContent, config); + Editor.content = [Editor.content, processedTemplateContent].filter(Boolean).join('\n'); + } + async function applyTemplate(newNote) { const templateFolder = await getTemplateFolder(); if (templateFolder == null) { - console.log(`addTemplate: templateFolder is null`); + console.log(`applyTemplate: warning: templateFolder is null`); await makeTemplateFolder(); await showMessage('Try using this command again to use a template'); return; @@ -9601,8 +9713,8 @@ lastName = "Doe" label: note.title, value: note }).filter(Boolean); - console.log(`addTemplate: found ${options.length} options`); - console.log(`addTemplate: asking user which template:`); + console.log(`applyTemplate: found ${options.length} options`); // console.log(`applyTemplate: asking user which template:`) + const selectedTemplate = await chooseOption('Choose Template', options); let templateContent = selectedTemplate?.content; @@ -9642,15 +9754,15 @@ lastName = "Doe" if (!title) { console.log('\tError: undefined or empty title'); - await showMessage('Cannot create a not with an empty title'); + await showMessage('Cannot create a note with an empty title'); return; } const templateFolder = await getTemplateFolder(); - let shouldAddTemplate = false; + let shouldApplyTemplate = false; if (templateFolder != null || templateFolder !== '') { - shouldAddTemplate = await chooseOption('Do you want to get started with a template?', [{ + shouldApplyTemplate = await chooseOption('Do you want to get started with a template?', [{ label: 'Yes', value: true }, { @@ -9659,8 +9771,8 @@ lastName = "Doe" }], false); } - if (shouldAddTemplate) { - await addTemplate([title, folder]); + if (shouldApplyTemplate) { + await applyTemplate([title, folder]); return; } @@ -9675,7 +9787,8 @@ lastName = "Doe" Editor.content = `# ${title}\n`; } - exports.addTemplate = addTemplate; + exports.applyNamedTemplate = applyNamedTemplate; + exports.applyTemplate = applyTemplate; exports.newNoteWithTemplate = newNoteWithTemplate; Object.defineProperty(exports, '__esModule', { value: true }); diff --git a/nmn.Templates/src/index.js b/nmn.Templates/src/index.js index aa6960ddf..a95b8d0b6 100644 --- a/nmn.Templates/src/index.js +++ b/nmn.Templates/src/index.js @@ -10,11 +10,42 @@ import { processTemplate } from './interpolation' import { getTemplateFolder, makeTemplateFolder } from './template-folder' -export async function addTemplate(newNote?: [string, string]) { +export async function applyNamedTemplate(templateName: string) { + console.log(`applyNamedTemplate: for template '${templateName}'`) + const templateFolder = await getTemplateFolder() + if (templateFolder == null) { + console.log(`\twarning: templateFolder is null`) + await makeTemplateFolder() + await showMessage('Try using this command again to use a template') + return + } + + const selectedTemplate = DataStore.projectNoteByTitle(templateName, true, false)[0] + + let templateContent = selectedTemplate?.content + if (templateContent == null || templateContent.length === 0) { + console.log(`\twarning: template '${templateName}' is null or empty`) + return + } + templateContent = templateContent.split('\n---\n').slice(1).join('\n---\n') + const config = (await getDefaultConfiguration()) ?? {} + + const processedTemplateContent = await processTemplate( + templateContent, + config, + ) + + Editor.content = [Editor.content, processedTemplateContent] + .filter(Boolean) + .join('\n') +} + +export async function applyTemplate(newNote?: [string, string]) { + const templateFolder = await getTemplateFolder() if (templateFolder == null) { - console.log(`addTemplate: templateFolder is null`) + console.log(`applyTemplate: warning: templateFolder is null`) await makeTemplateFolder() await showMessage('Try using this command again to use a template') return @@ -28,9 +59,9 @@ export async function addTemplate(newNote?: [string, string]) { ) .filter(Boolean) - console.log(`addTemplate: found ${options.length} options`) + console.log(`applyTemplate: found ${options.length} options`) - console.log(`addTemplate: asking user which template:`) + // console.log(`applyTemplate: asking user which template:`) const selectedTemplate = await chooseOption( 'Choose Template', options, @@ -84,15 +115,15 @@ export async function newNoteWithTemplate() { if (!title) { console.log('\tError: undefined or empty title') - await showMessage('Cannot create a not with an empty title') + await showMessage('Cannot create a note with an empty title') return } const templateFolder = await getTemplateFolder() - let shouldAddTemplate = false + let shouldApplyTemplate = false if (templateFolder != null || templateFolder !== '') { - shouldAddTemplate = await chooseOption( + shouldApplyTemplate = await chooseOption( 'Do you want to get started with a template?', [ { label: 'Yes', value: true }, @@ -101,8 +132,8 @@ export async function newNoteWithTemplate() { false, ) } - if (shouldAddTemplate) { - await addTemplate([title, folder]) + if (shouldApplyTemplate) { + await applyTemplate([title, folder]) return } diff --git a/nmn.Templates/src/interpolation.js b/nmn.Templates/src/interpolation.js index d63b3f14a..7ea5ca16a 100644 --- a/nmn.Templates/src/interpolation.js +++ b/nmn.Templates/src/interpolation.js @@ -1,12 +1,14 @@ // @flow import { getInput } from '../../nmn.sweep/src/userInput' +import { getWeatherSummary } from './weather' import { parseJSON5 } from './configuration' export async function processTemplate( content: string, config: { [string]: ?mixed }, ): Promise { + console.log(`processTemplate: ${content}`) const tagStart = content.indexOf('{{') const tagEnd = content.indexOf('}}') const hasTag = tagStart !== -1 && tagEnd !== -1 && tagStart < tagEnd @@ -19,7 +21,7 @@ export async function processTemplate( const tag = content.slice(tagStart + 2, tagEnd) try { - const tagProcessed = await processTag(tag, config) + const tagProcessed = await processTags(tag, config) const restProcessed = await processTemplate(afterTag, config) return beforeTag + tagProcessed + restProcessed } catch (e) { @@ -28,13 +30,34 @@ export async function processTemplate( } } -async function processTag( +// Apply any matching tag functions +export async function processTags( tag: string, config: { [string]: ?mixed }, ): Promise { + console.log(`processTag: ${tag}`) if (tag.startsWith('date(') && tag.endsWith(')')) { return await processDate(tag.slice(5, tag.length - 1), config) } + else if (tag.startsWith('weather(') && tag.endsWith(')')) { + return await getWeatherSummary(tag.slice(8, tag.length - 1), config) + } + + // **Add other extension function calls here** + // Can call functions defined in other plugins, by appropriate use + // of imports at top of file (e.g. getWeatherSummary) + // Or declare below (e.g. processDate) + + else { // no matching funcs, so now attempt to match defined tag values instead + return processTagValues(tag, config) + } +} + +// Apply any matching tag values, asking user for value if not found in configuration +async function processTagValues( + tag: string, + config: { [string]: ?mixed }, +): Promise { const valueInConfig = tag // eslint-disable-next-line no-useless-escape .split(/[\.\[\]]/) @@ -50,18 +73,23 @@ async function processTag( return await getInput(`Value for ${tag}`) } +// ---------------------------------------------------------------- +// Define new tag functions here ... + async function processDate( - dateConfig: string, + dateParams: string, config: { [string]: ?mixed }, ): Promise { + console.log(`processDate: ${dateConfig}`) const defaultConfig = config.date ?? {} - const paramConfig = dateConfig.trim() ? await parseJSON5(dateConfig) : {} - // console.log(`param config: ${dateConfig} as ${JSON.stringify(paramConfig)}`); + const paramConfig = dateParams.trim() ? await parseJSON5(dateParams) : {} + // console.log(`param config: ${dateParams} as ${JSON.stringify(paramConfig)}`); const finalArguments: { [string]: mixed } = { ...defaultConfig, ...paramConfig, } + // ... = "gather the remaining parameters into an array" const { locale, ...otherParams } = (finalArguments: any) const localeParam = locale != null ? String(locale) : [] diff --git a/nmn.Templates/src/template-folder.js b/nmn.Templates/src/template-folder.js index 2738c8a06..9e8ac24f0 100644 --- a/nmn.Templates/src/template-folder.js +++ b/nmn.Templates/src/template-folder.js @@ -115,7 +115,20 @@ The first code-block within the note will always be used. So edit the default co // can be "short", "medium", "long" or "full" dateStyle: 'short', // optional key, can be "short", "medium", "long" or "full" - timeStyle: 'short, + timeStyle: 'short', + }, + + // configuration for weather data + weather: { + // API key for https://openweathermap.org/ + // !!REQUIRED!! + openWeatherAPIKey: '... put your API key here ...', + // Default location for weather forcast + latPosition: 0.0, + longPosition: 0.0, + // Default units. Can be 'metric' (for Celsius), or 'metric' (for Fahrenheit) + openWeatherUnits: 'metric', + // When using a weather tag, you can customize these options. }, // default values for custom tags. @@ -150,6 +163,18 @@ date: # can be null (to skip time), "short", "medium", "long" or "full" timeStyle: short +# configuration for weather data lookups, if wanted +weather: + # API key for https://openweathermap.org/ + # !!REQUIRED!! + openWeatherAPIKey: + # Default location for weather forcast + latPosition: 0.0 + longPosition: 0.0 + # Default units. Can be 'metric' (for Celsius), or 'metric' (for Fahrenheit) + openWeatherUnits: metric + # When using a weather tag, you can customize these options. + # default values for custom tags. # These tags cannot be functions, but you may choose to have nested objects. # feel free to edit this value however you see fit. @@ -178,6 +203,18 @@ dateStyle = "short" # can be null (to skip time), "short", "medium", "long" or "full" timeStyle = "short" +// configuration for weather data +[weather] +// API key for https://openweathermap.org/ +# !!REQUIRED!! +openWeatherAPIKey = +# Default location for weather forcast +latPosition = 0.0 +longPosition = 0.0 +# Default units. Can be 'metric' (for Celsius), or 'metric' (for Fahrenheit) +openWeatherUnits = 'metric' +# When using a weather tag, you can customize these options. + # default values for custom tags. [tagValue] # These tags cannot be functions, but you may choose to have nested objects. @@ -190,22 +227,3 @@ firstName = "John" lastName = "Doe" \`\`\` ` - -/** - * - * The following should be added to the default configuration - * once the weather function works. - * - // configuration for weather data - weather: { - // API key for https://openweathermap.org/ - // !!REQUIRED!! - apiKey: '... put your API key here ...', - // Default location for weather forcast - lattitude: 0, - longitude: 0, - // Default temperature unit. Can be "C" (Celcius), "K" (Kelvin) or "F" (Fahrenheit) - unit: 'C', - // When using a weather tag, you can customize these options. - }, -*/ diff --git a/jgclark.DailyJournal/src/weather.js b/nmn.Templates/src/weather.js similarity index 96% rename from jgclark.DailyJournal/src/weather.js rename to nmn.Templates/src/weather.js index 9a0fca427..e20e114b4 100644 --- a/jgclark.DailyJournal/src/weather.js +++ b/nmn.Templates/src/weather.js @@ -44,10 +44,9 @@ export async function getWeatherSummary( const pref_longPosition = weatherConfig.longPosition const pref_openWeatherUnits = weatherConfig.openWeatherUnits - // TODO: probably getDefaultConfiguration rather than parseJSON5 ? console.log(`getWeatherSummary: Params: '${weatherParams}'`) const paramConfig = weatherParams.trim() ? await parseJSON5(weatherParams) : {} - console.log(paramConfig) + // console.log(paramConfig) const getWeatherURL = `https://api.openweathermap.org/data/2.5/onecall?lat=${ @@ -59,15 +58,13 @@ export async function getWeatherSummary( }&appid=${ pref_openWeatherAPIKey}` - // TODO: Allow for more customisation of what is pulled out from the API's data structure - // using weatherParams - const jsonIn = await fetch(getWeatherURL) if (jsonIn != null) { const weatherTodayAll = JSON.parse(jsonIn).daily["0"] const maxTemp = weatherTodayAll.feels_like.day.toFixed(0) const minTemp = weatherTodayAll.feels_like.night.toFixed(0) const weatherDesc = weatherTodayAll.weather["0"].description + // see if we can fix an icon for this as well, according to returned description. Main terms are: // thunderstorm, drizzle, shower > rain, snow, sleet, clear sky, mist, fog, dust, tornado, overcast > clouds // with 'light' modifier for rain and snow @@ -78,9 +75,14 @@ export async function getWeatherSummary( break } } + + // TODO: Allow for more customisation of what is pulled out from the API's data structure + // using weatherParams + const summaryLine = `${maxTemp}/${minTemp} ${weatherIcon}${weatherDesc}` console.log(`\t${summaryLine}`) return summaryLine + } else { await showMessage('Sorry; error in Weather lookup') return 'sorry; error in Weather lookup' diff --git a/package.json b/package.json index 341c4ccb8..c33dc8689 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "build:jgclark.RepeatExtensions": "rollup -c ./jgclark.RepeatExtensions/rollup.config.js", "build:jgclark.CalendarHelpers": "rollup -c ./jgclark.CalendarHelpers/rollup.config.js", "build:jgclark.QuickCapture": "rollup -c ./jgclark.QuickCapture/rollup.config.js", + "build:jgclark.NoteHelpers": "rollup -c ./jgclark.NoteHelpers/rollup.config.js", + "build:jgclark.GTD": "rollup -c ./jgclark.GTD/rollup.config.js", "build": "concurrently \"npm:build:*\"", "watch:nmn.sweep": "rollup -c ./nmn.sweep/rollup.config.js --watch", "watch:nmn.Templates": "rollup -c ./nmn.Templates/rollup.config.js --watch", @@ -40,6 +42,8 @@ "watch:jgclark.RepeatExtensions": "rollup -c ./jgclark.RepeatExtensions/rollup.config.js --watch", "watch:jgclark.CalendarHelpers": "rollup -c ./jgclark.CalendarHelpers/rollup.config.js --watch", "watch:jgclark.QuickCapture": "rollup -c ./jgclark.QuickCapture/rollup.config.js --watch", + "watch:jgclark.NoteHelpers": "rollup -c ./jgclark.NoteHelpers/rollup.config.js --watch", + "watch:jgclark.GTD": "rollup -c ./jgclark.GTD/rollup.config.js --watch", "watch": "concurrently \"npm:watch:*\"", "autowatch": "node scripts/rollup.js", "lint": "eslint .",