Migrating from Obsidian-Tasks

@Pietzaahh This is a great script.
We have so many options that I don’t know which one to use these days :sweat_smile:

Anyhow, I checked back with my colleague “Gemini” and it gave me back this script with priorities working.
It also refactored a bit more the code.

Hope you can use it to continue implementing your ideas:

```space-script
const rrule = import("https://esm.sh/[email protected]")

const checkbox = /^\[([^\]]+)]\s+/
const hashTags = /(^|\s)#[^ !@#$%^&*(),.?":{}|<>]+/g
const taskId = /[a-zA-Z0-9-_]+/.source

// Priority emojis and their mappings
const priorityEmojis = {
   "❗": 1, // highest
   "⏫": 2, // high
   "🔼": 3, // medium
   "🔽": 4, // low
   "⏬": 5, // lowest
}

const matchPriority = Object.keys(priorityEmojis).join("|")

// Task-related emojis for date attributes  
const taskEmojis = {
    "🛫": "start",    
    "➕": "created",
    "⏳": "scheduled", 
    "📅": "deadline",
    "✅": "completed",
    "❌": "cancelled"
};

// Matchers including priority and additional task emojis
const matchers = {
  priority: new RegExp(`\\s+(${matchPriority})`, "u"),
  ...Object.fromEntries(Object.entries(taskEmojis).map(
     ([emoji, attribute]) => [attribute, new RegExp(`\\s*${emoji}\\s*(\\d{4}-\\d{2}-\\d{2})`, "u")]
  )),
  recurrence: /\s+🔁\s*([a-zA-Z0-9, !]+)/iu,
  depends: new RegExp(`\\s+⛔️\\s*(${taskId}( *, *${taskId} *)*)`, "iu"),
  id: new RegExp(`\\s+🆔\s*(${taskId})`, "iu"),
  tags: new RegExp(hashTags.source, "iu"),
}

const extractors = Object.entries(matchers).map(([prop, regex]) => [
  prop,
  new RegExp(regex.source + "$", regex.flags),
])

// Helper function to parse priorities from strings (fallback)
const parsePriority = (emoji) => emoji ? priorityEmojis[emoji] || 3 : 3; // Default to medium

// Extract attributes including priority and dates
const extractAttributes = (task) => {
  let attrs = { tags: [] }

  task = task.replace(checkbox, "").trim(); // Trim checkbox first

  for ([prop, regex] of extractors) {
    const match = regex.exec(task)
    if (!match) continue;

    const value = attrs[prop];
    if (Array.isArray(value)) value.push(match[1])
    else attrs[prop] = match[1] 

    task = task.slice(0, match.index).trimEnd();
  }

  // Prioritize emoji-based priorities
  attrs.priority = priorityEmojis[task.match(new RegExp(matchPriority))] || parsePriority(attrs.priority); 

  attrs.name = task.trim();
  return attrs;
}

silverbullet.registerAttributeExtractor({ tags: ["task"] }, (text) => {
  let { tags, ...attrs } = extractAttributes(text);

  // Tags stay at the end for feature parity
  if (tags.length) attrs.name += ` ${tags.join(" ")}`

  // If the task is cancelled, override the done state
  if (text.startsWith("[-]")) attrs.done = true

  return attrs
})

const appendDate = async (from, emoji) => {
    const now = Temporal.Now.plainDateISO().toString();
    await syscall("editor.dispatch", {
        changes: { from, insert: ` ${emoji} ${now}` },
    });
}

const finalDates = new RegExp(
  `${matchers.completed.source}|${matchers.cancelled.source}`,
  "gu"
)

const stateChanges = {
  // Completed
  "x": ({ to }) => appendDate(to, "✅"),
  // Incomplete, remove finalization dates from the task
  " ": async ({ from, text, to }) => {
    const insert = text.replaceAll(finalDates, "")
    if (insert === text) return 

    // The raw text includes the checkbox area which has the old task
    // state and we don't want that.
    const [current] = checkbox.exec(text) || [""]
    const offset = current.length
    await syscall("editor.dispatch", {
      changes: {
        from: from + offset, 
        to, 
        insert: insert.slice(offset),
      },
    })
  },
  // Cancelled
  "-": ({ to }) => appendDate(to, "❌")
}

// Add or remove finalization date when changing a task
silverbullet.registerEventListener(
  { name: "task:stateChange" },
  ({ data }) => stateChanges[data.newState]?.(data)
)

const toDateString = (date) => date
  .toTemporalInstant()
  .toZonedDateTimeISO(Temporal.Now.timeZoneId())
  .toPlainDate()

// Setup a new recurring task when one is completed.
silverbullet.registerEventListener(
  { name: "task:stateChange" },
  async ({ data }) => {
    if (data.newState !== "x") return
    const { from, text } = data
    const match = matchers.recurrence.exec(text)
    if (!match) return // non-recurring

    const [,deadline] = matchers.deadline.exec(text) || []
    if (!deadline) {
      return syscall(
        "editor.flashNotification",
        "No deadline found for recurrence"
      )
    }

    const { RRule } = await rrule
    const start = new Date(`${deadline}T00:00:00`)
    const rule = new RRule({
      ...RRule.parseText(match[1].trimEnd()),
      dtstart: start,
    })
    const next = rule.after(start)
    const nextLine = text.replace(
      matchers.deadline,
      ` 📅 ${toDateString(next)}`
    )

    await syscall("editor.insertAtPos", `${nextLine}\n- `, from)
  }
)
```

One thing that would be great to improve is to update “cancelled” tasks without the need to click on the - [-] part, just when the - is inserted, at change task state.
I tried to fix that but it didn’t work out.

I tried to add the following, in case it matters:

// Cancelled
"-": async ({ to }) => {  // Changed to async
    await appendDate(to, "❌"); // Directly append the date
}