(Script) Display a calendar for Journal entries

Hi,

I just played around a bit the last days and implemented a feature on my (very short) Silverbullet wishlist, and I thought, it might be handy for others, too.
As I’m not familiar with writing plugins, yet, I’m sharing my codeblocks at first. You will have to alter it a bit to fit your own preferences/directories/Language.

Hint: My Journal contains files with this naming-format: /Journal/2024-11-22_Fr

The output looks like this (all markdown based):

Every day is a link to a page. If page exists already, it is marked in bold.
Current day is marked with “(xx)”.
You can add marks for other important days, too (marked with “!”). This is defined with a list of days as secon

First the space-script itself. Can be placed on any page, it defines the needed functions:

silverbullet.registerFunction({name: "generateCalendarMarkdown"}, async (year, month, markdays, today,nohead) => {

    const monthNames = [
        'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 
        'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
    ];
    const daysOfWeek = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
    const date = new Date(year, month - 1, 1);
    let markdown = ""
      if (! nohead ) {markdown = `## Journal Kalender: ${monthNames[month - 1]} - ${month} / ${year}\n\n`;}
    markdown += '| ' + daysOfWeek.join(' | ') + ' |\n';
    markdown += '| ' + '--- |'.repeat(7) + '\n';

    // Adjust the start day for Monday
    let startDay = (date.getDay() + 6) % 7;

    // Fill initial empty cells
    for (let i = 0; i < startDay; i++) {
        markdown += '|    ';
    }

    while (date.getMonth() === month - 1) {
        let day = date.getDate();

        let daystr = String(day).padStart(2, ' ');

        let datePage = "Journal/" + String(year) + "-" + String(month).padStart(2, '0') + "-" + String(day).padStart(2, '0') + "_" + daysOfWeek[date.getDay()-1] 
      
        if (await space.fileExists(datePage + ".md")) {  //datePage
          daystr = "**[[" + datePage + "|" + String(day) + "]]**";
        } else {
          daystr = "[[" + datePage + "|" + String(day) + "]]";
        }

        if ( (today === undefined && day === Temporal.Now.plainDateISO().day ) || (today === day) ){
            markdown += '|' + " (" + daystr + '' + ") " ;
        }
        else if (markdays.includes(day) ) {
            markdown += '|' + " ! " + daystr + '' + " !" ;
        } else {
            markdown += '| ' + daystr + ' ';
        }
        if ((date.getDay() + 6) % 7 === 6) {
            markdown += '|\n';
        }
        date.setDate(date.getDate() + 1);
    }

    // Fill trailing empty cells
    if ((date.getDay() + 6) % 7 !== 0) {
        for (let i = (date.getDay() + 6) % 7; i < 7; i++) {
            markdown += '|    ';
        }
        markdown += '|\n';
    }

    return markdown;
});

You display the calendar on a page with this query, with these options:

  1. year
  2. month
  3. list of dates to mark extra (e.g. [23,11,26]), e.g. Birthdays, etc. in current month
  4. define another date to mark for today, if undefined take actual today. Useful, e.g. if you want to mark the date of a older Journal page
  5. if set to true, do not render a heading
{{generateCalendarMarkdown(2024,11,[23,11,26],undefined,false)}}

On my todo list

  • creating this as plugin
  • better formatting (size, color) with CSS / space-style
  • adding more special markings
  • make it language independent (as it is currently in german, only)
    (more ideas, hints and help welcome)

Have fun :slight_smile:
Nico

16 Likes

Oh my goodness, thank you SO much for this! I was just lamenting in my introduction post that I didn’t think I’d be able to fully migrate over from Obsidian yet because I didn’t see a calendar feature or plug… I didn’t realize the scripting here is so powerful that you can achieve this just with a single embedded code block! Incredible!

Now on my to do list is to see if I can extend this to incorporate weekly and monthly notes, hmmm… (Well, after I finish getting the SB fundamentals down!)

2 Likes

Well, did I mention, I fell in love with LUA?

So I created a new version of the calendarscript in lua.
As I’m using html directly this time (instead of markdown output) it looks better (IMO) and you are able to style it somewhat.

Please keep in mind, tha tyou have to use the latest Silverbullet EDGE.

Screenshot:

For now only the actual day is marked and every day for which a page exists is displayed a bit different.

To display the calendar on a page, use this command:
${generate_calendar(year, month)},
e.g.: ${generate_calendar(2025, 2)}
(actual date is displayed, if year and/or month left empty)

To “install” it, simple create a new page (e.g. in your scripts subdirectory) with this content:

```space-lua
function generate_calendar(year, month)
    -- use actual date, if nothing given as parameter
    if year == nil or month == nil then
       year = tonumber(os.date("%Y"))
       month = tonumber(os.date("%m"))
    end
  
    local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
    local month_names = {"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"}
    local day_names = {"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"}
    
    -- Check for leap year
    if (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0) then
        days_in_month[2] = 29
    end
    
    local days = days_in_month[month]
    
    -- Get the first day of the month (1 = Sunday, 2 = Monday, ..., 7 = Saturday)
    local first_day = tonumber(os.date("%w", os.time{year=year, month=month, day=1}))+1
    
    -- Adjust first day to start from Monday (1 = Monday, ..., 7 = Sunday)
    first_day = first_day - 1
    if first_day == 0 then
        first_day = 7
    end
    
    local html = "<table border='1'>\n"
    html = html .. "<tr><th colspan='7'>" .. month_names[month] .. " " .. year .. "</th></tr>\n"
    html = html .. "<tr>"
    for _, day_name in ipairs(day_names) do
        html = html .. "<th>" .. day_name .. "</th>"
    end
    html = html .. "</tr>\n"
    
    local day = 1
    local started = false
    local journalfilename =""
    local cellcontent =""
    for week = 1, 6 do
        html = html .. "<tr>"
        for weekday = 1, 7 do
            if not started and weekday == first_day then
                started = true
            end
            if started and day <= days then

              -- define Jounral directory and Filenames
              journalfilename = "Journal/" .. year .. "-" .. leadingzero(month) .. "-" .. leadingzero(day) .. "_" .. day_names[weekday]

              if space.fileExists(journalfilename .. ".md") then
                 cellcontent = "<b><a href=\"" .. journalfilename .. "\" class=\"mark\"  data-ref=\"" .. journalfilename .. "\">" ..  day .. "</a></b>"
              else
                 cellcontent = "<a href=\"" .. journalfilename .. "\"  data-ref=\"" .. journalfilename .. "\">" ..  day .. "</a>"
              end
              
              -- mark today
              if day == tonumber(os.date("%d")) and month == tonumber(os.date("%m"))  and year == tonumber(os.date("%Y")) then
                html = html .. "<td class=\"mark\">(" .. cellcontent .. ")</td>"
              else
                html = html .. "<td>" .. cellcontent .. "</td>"
              end
                
                day = day + 1
            else
                html = html .. "<td></td>"
            end
        end
        html = html .. "</tr>\n"
        if day > days then
            break
        end
    end
    
    html = html .. "</table>"
    
    return {
      html=html;
      display="block";
      cssClasses={"calendartable"}
    }
end

local function leadingzero(number)
   return (number < 10 and "0" .. number) or tostring(number)
end

Styles:

```space-style
.calendartable {
  /* color: blue; */
  border-collapse: collapse; 
  width: 50%;
}

.calendartable th {
  border: 1px solid black;
  padding: 3px; 
  text-align: left;  
  background-color: lightgray;
}

.calendartable td {
  border: 1px solid gray; 
  padding: 3px; 
  text-align: left;  
}

.calendartable td.mark {
  background-color: orange;
}

.calendartable a {
 text-decoration-line: none;
 color: black;
}

.calendartable a.mark {
 text-decoration-line: none;
 color: blue;
}

EDIT:
This script and future editions will is available on github from now on:

5 Likes

Would you like to turn this to a plugin?

Similar to this GitHub - xyhp915/logseq-journals-calendar: A journals calendar for Logseq.

Or host it somewhere as an external library :innocent:

If you don’t have a GitHub-account yourself, I can also host it in my external library repository with a reference to this topic/credits to you. :slight_smile:

Well, basically there is not much to be said against it…

However, I (currently) have no clue, yet, how to transform that into a PLUG.

Providing that as “direct download” via Github would be possible nearly immediately, as I do have an own account.

My only concern is, that my scripts are a bit premature, in my opinion.
They are set to german habits for example (week starts at monday).

So I’m not sure, if they need more “finishing” before turning into a plugin.

Currently I was happy to serve them this via this forum post, as anyone could simply change everything to fit their needs by simply copy the code to a page and alter it afterwards…

For directly importing via PLUG or Library-import, I personally rather have some kind of configuration file to change the functions…

What do you think?

On the other hand:

  • is it possible to turn LUA based scripts to PLUGs, anyway? Or does this work only with space-scripts or even typescript only?
  • How about the Library import? Possible with LUA, too? Am I understand it correct, that it is enough to publish a page.md with space-lua and space-style as content (accompanied by some json-file for the importscript)?

I definitely will have to have a deeper look into both possible ways, soon :slight_smile:

2 Likes

Yeah, for the external library all you need is a repo with the following:

/index.json to tell the plug what is there to download for a specific folder.
/Library/ directory with another directory for this specific script.

For example:

/
  - index.json
  - Library/
      - JournalCalendar/
          - calendar.md  # with the space-lua and space-style blocks inside.

The page can have anything on it that’s supported in your own space (space-script, style, lua, templates, queries, etc.)

Not sure how you would go about an easy way to configure this. You can probably read the settings page from Lua? Then you can default to the German days, but let users override it in their settings.

Based on @zeus-web 's excellent contribution, I’ve moved the configuration options to the top for easier customization and added support for defining the journal file path using a pattern. Additionally, there’s now an option to start the week on Sunday and proper handling for leap years (just in case).

-- Configuration: Customize these settings
local week_start_on_monday = true -- Set to false for Sunday start
local journal_path_pattern = "Journal/%year%/%month%/%year%-%month%-%day%_%weekday%"
local month_names = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
local day_names = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} -- Used when the week starts on Monday


function generate_calendar(year, month)
    -- Use current date if no parameters are provided
    if year == nil or month == nil then
        year = tonumber(os.date("%Y"))
        month = tonumber(os.date("%m"))
    end

    -- Determine day names based on week start setting
    local selected_day_names = week_start_on_monday and day_names or -{day_names[7], day_names[1], day_names[2], day_names[3], day_names[4], day_names[5], day_names[6]}

    local days = get_days_in_month(year, month)

    -- Get the first day of the month (1 = Sunday, 2 = Monday, ..., 7 = Saturday)
    local first_day = tonumber(os.date("%w", os.time{year=year, month=month, day=1})) + 1

    -- Adjust the first day based on the week start setting
    if week_start_on_monday then
        first_day = first_day - 1
        if first_day == 0 then
            first_day = 7
        end
    end

    local html = "<table border='1'>\n"
    html = html .. "<tr><th colspan='7'>" .. month_names[month] .. " " .. year .. "</th></tr>\n"
    html = html .. "<tr>"
    for _, day_name in ipairs(selected_day_names) do
        html = html .. "<th>" .. day_name .. "</th>"
    end
    html = html .. "</tr>\n"

    local day = 1
    local started = false
    for week = 1, 6 do
        html = html .. "<tr>"
        for weekday = 1, 7 do
            if not started and weekday == first_day then
                started = true
            end
            if started and day <= days then
                -- Generate journal file path using the defined pattern
                local journalfilename = generate_journal_path(year, month, day, weekday, journal_path_pattern)

                local cellcontent = ""
                if space.fileExists(journalfilename .. ".md") then
                    cellcontent = "<b><a href=\"" .. journalfilename .. "\" class=\"mark\" data-ref=\"" .. journalfilename .. "\">" .. day .. "</a></b>"
                else
                    cellcontent = "<a href=\"" .. journalfilename .. "\" data-ref=\"" .. journalfilename .. "\">" .. day .. "</a>"
                end

                -- Mark today's date
                if day == tonumber(os.date("%d")) and month == tonumber(os.date("%m")) and year == tonumber(os.date("%Y")) then
                    html = html .. "<td class=\"mark\">(" .. cellcontent .. ")</td>"
                else
                    html = html .. "<td>" .. cellcontent .. "</td>"
                end

                day = day + 1
            else
                html = html .. "<td></td>"
            end
        end
        html = html .. "</tr>\n"
        if day > days then
            break
        end
    end

    html = html .. "</table>"

    return {
        html = html,
        display = "block",
        cssClasses = {"calendartable"}
    }
end

-- Local function to generate journal file path using the defined pattern
local function generate_journal_path(year, month, day, weekday, pattern)    
    -- Ensure we use leading zero format where necessary
    local replacements = {
        ["%%year%%"] = tostring(year),
        ["%%month%%"] = leadingzero(month),
        ["%%day%%"] = leadingzero(day),
        ["%%weekday%%"] = day_names[weekday]
    }

    -- Replace placeholders in the pattern
    for key, value in pairs(replacements) do
        pattern = string.gsub(pattern, key, value)
    end

    return pattern
end
-- Local function to ensure two-digit formatting
local function leadingzero(number)
    return (number < 10 and "0" .. number) or tostring(number)
end

-- Local function to get the correct number of days in a month (handles leap years)
local function get_days_in_month(year, month)
    local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
    
    -- Check for leap year and adjust February
    if month == 2 and ((year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0)) then
        return 29
    end
    
    return days_in_month[month]
end

1 Like

Thanks, Tim!
I like the progress we do here :slight_smile:

A logical consequence is now to define the variables through space-config (or similar).

But I think, this is not fully implemented in Space-lua. I found “Library/Std/Config”, but that does not integrate space-settings, as far as I understand.

BTW:
Currently I’m working on moving my (LUA-)script to github to be used with ExternalLibraries PLUG (like mentioned above).
Do you mind, if I integrate your changes as well?

This script (as more advanced LUA-version!) and future editions/updates is available on github from now on:

Feel free to use it, fork, edit, suggest changes, do PR etc. :slight_smile:

3 Likes

Sure. Most of them are from yours.

1 Like

Thanks, @zeus-web

This indeed also makes it easier to track and propose changes. :slight_smile:
(I already submitted a PR to use CSS variables for colouring)

Thanks, Tim Ma!
I added your code with some changes, though.

There was an error, if week does NOT start at Monday. (I think, there was a “minus” in your code after “or”, that is not needed:

local selected_day_names = week_start_on_monday and day_names or -{day_names[7], day_names[1], day_names[2], day_names[3], day_names[4], day_names[5], day_names[6]}

After removing that, It seems to work as expected :slight_smile:

Everything is now updated on github, too. (see link in this topic)

2 Likes

Just sign up on this forum to say, it’s awesome! I just setup SB two days ago, your solution is not only useful, by reading the comments I also learnt how flexible SB can be, and how powerful is Lua.
Thank you!

3 Likes

Welcome to Silverbullet!! Have fun. :grinning:

1 Like