--- type: Specification title: Joplin Shopping List Plugin — Specification description: Functional specification for the store-grouped shopping list editor view in Joplin. resource: https://github.com/lansidev/joplin-shopping-list tags: [joplin, plugin, specification, shopping-list] timestamp: 2026-06-14T00:00:00Z --- # Overview The Shopping List plugin renders a Joplin Markdown note as an interactive, store-grouped shopping list. Joplin's built-in editor-toggle button switches between the normal Markdown editor and the plugin's view; it appears automatically for notes that opt in via the marker. In the plugin view, a text input sits on top and the items appear below it, grouped under their store. Typing an item and pressing Enter routes it to the correct store and sorts it into the user's in-store walking order, using regex patterns defined in the settings. Items can be ticked off (which hides them from the view but keeps them in the Markdown) and pushed to the next matching store. # Goals & non-goals **Goals** - Edit one note that contains several store sections (e.g. `## Aldi`, `## DM`, `## Rewe`). - Configure, in the plugin settings, an ordered list of stores, each with an ordered list of regex patterns (duplicates allowed) that match items. - Auto-route and auto-sort items by those patterns so the list follows the in-store walking path. - Move an item to the next matching store; tick items off; hide ticked items in the view only. **Non-goals** - Managing multiple separate shopping notes at once (the view always reflects the current note). - A drag-and-drop reordering UI (ordering is derived from the configured patterns). - Editing the regex configuration per note (configuration is global, in settings). # Concepts - **Marker** — a note opts in to the shopping view by containing the HTML comment ``. It is invisible in the rendered Markdown. **Tools → Enable shopping list for this note** inserts it. - **Store** — a named group, defined both in the note as a `## Store` section and in the settings as an ordered list of terms. Stores are matched between note and settings by name (case-insensitive). - **Term** — a regular expression for a product name (write singular + plural yourself, e.g. `Kartoffeln?`). It is compiled so it also matches an optional leading/trailing **quantity**, hence matching the whole item string. The position of the first matching term within a store determines an item's sort order (the walking path). - **Unit** — a recognised quantity word (e.g. `kg`, `ml`, `Dose(n)?`), configured in a separate field. A number — with or without a unit, before or after the term — is matched automatically. - **Other** — a bucket section (`## Other`) for items that match no configured store. # Note format ```markdown # Einkaufsliste ## Aldi - [ ] 8 Äpfel - [ ] 2,5kg Kartoffeln - [ ] Käse ## DM - [ ] Waschmittel - [x] Proteinriegel ## Rewe - [ ] Tofu ``` - The title and the marker live in the *preamble* (everything before the first `## ` heading) and are preserved verbatim. - A `## Heading` (level 2+) starts a store section. Items are GitHub-style task list lines (`- [ ] text` / `- [x] text`; bullets `-`, `*`, `+` are accepted). - Checked (`[x]`) items remain in the Markdown but are hidden in the plugin view; the trash button in the top bar deletes them in bulk (after a confirmation). # Configuration DSL Stores and units are configured with a small line-based format, edited via **Tools → Edit shopping list configuration** (two fields in one dialog). It mirrors the note layout, so no JSON/backslash escaping is needed. **Stores** — one `## StoreName` header per store, then one product **term** per line: ``` ## Aldi Apfel|Äpfel Kartoffeln? Käse ## DM Waschmittel ## Rewe Tofu ``` - A `## StoreName` line starts a store; the top store is first in walking order. - Every other non-blank line is one **term** — a regex for the product name; write singular and plural yourself. Duplicate terms are allowed. - Each term is compiled anchored and case-insensitive, wrapped so an optional leading/trailing quantity is matched automatically — roughly `^(?:? ?)$` with the `i` flag. So `Äpfel` matches `Äpfel`, `8 Äpfel` and `2,5kg Äpfel`, but not `8 Äpfeln`. - Invalid regexes are reported with their line number and the edit dialog stays open. **Units** — one unit per line, in a separate field; each may itself be a regex: ``` g kg mg l ml Stück Dose(n)? Bund ``` - A *quantity* is a number (integer or decimal, `.` or `,`), optionally followed by one of these units: `8`, `2,5kg`, `500 g`, `1l`, `2 Dosen`. - Units are global (one set for all stores) and matched case-insensitively. With no units configured, only a bare number is recognised. # Behaviour - **Activation** — the view is available only for notes containing the marker (`onActivationCheck`). Joplin then shows its built-in editor-toggle button to switch between the Markdown editor and this view; no custom toolbar button is registered. - **Add** — on Enter, the item is routed to the **first** store (in configured order) that has any matching term; if none match, it goes to **Other**. - **Sort** — within a configured store, items are stably sorted by the index of their first matching term; unmatched items go to the end. Sorting is re-applied on every change and written back to the Markdown. - **Move to next store** — the down-arrow on a row moves the item to the next store in its *move cycle*: - items matching one or more stores cycle through *those* stores in configured order and **wrap** back to the first; - items matching no store cycle through *all* stores and then **Other**, and wrap. - The arrow is hidden when there is only one possible destination. - **Check / hide** — the left checkbox toggles `[ ]`/`[x]`. Checked items disappear from the view but stay in the Markdown; un-check them from the Markdown editor. - **Section count** — each store heading shows an `(open/total)` badge, where *open* is the number of unchecked items still visible and *total* counts every item in the section (open + bought). A fully bought section drops out of the view, so the badge never reads `(0/n)`. - **Delete bought items** — a trash button in the top bar removes every checked (`[x]`) item from the note in one step. It is enabled only when there is at least one bought item, and asks for confirmation first (an OK/Cancel message box, which also works on Joplin mobile). A store section left with no items afterwards is removed too, so its heading does not linger. # Settings | Setting | Type | Visibility | Purpose | | --- | --- | --- | --- | | Store configuration | String | Non-public | Stores + terms DSL (source of truth). Seeded with an example. | | Quantity units | String | Non-public | Recognised units, one per line. Seeded with the defaults. | Both settings are **non-public** (hidden from the settings screen): Joplin renders a String setting as a single-line input, which is unusable for this multi-line content. Editing is done entirely through the **Shopping List: Edit configuration** dialog — two fields (stores and units), each a textarea with validation, opened from the Tools menu. # Design decisions | Decision | Choice | Rationale | | --- | --- | --- | | Note recognition | Explicit `` marker | Predictable opt-in; invisible in rendered Markdown. | | Configuration UX | Line-based DSL edited in a dialog | No JSON escaping; mirrors the note; supports order + duplicates. | | Move past last store | Wrap to first matching store | Lets the user cycle an item through all candidates. | | No-match items | Dedicated "Other" section | Nothing is silently dropped; items can be moved out manually. | # Acceptance criteria 1. A note with the marker shows Joplin's editor-toggle button; a note without it does not. 2. Typing `2 Äpfel` places it in Aldi directly after `8 Äpfel` (same first pattern). 3. An item present in all stores (e.g. peanut butter) lands in the first store and cycles Aldi → DM → Rewe → Aldi via the down-arrow. 4. Checking an item hides it in the view; the `- [x]` line is still present in the Markdown editor. 5. An item matching no store appears under "Other". 6. The trash button is disabled when there are no bought items; with some, clicking it asks for confirmation and, on OK, removes every `[x]` line from the note (and any now-empty store heading) while leaving unchecked items untouched. 7. A store heading with one bought and two open items reads `DM (2/3)`; checking another item updates it to `(1/3)`, and the section disappears once all three are bought. 8. The build (`npm run dist`) produces an installable `.jpl`; `npm test` passes. # Open questions / future work - Optional "show completed" toggle to un-check items from within the view. - Per-note store ordering overrides. - Configurable name for the "Other" bucket. - The manifest declares mobile support (`platforms: [desktop, mobile]`) and the APIs used are cross-platform; still, verify the editor view end-to-end on Joplin mobile (iOS/Android) on a device. # Citations [1] [Joplin plugin: editor views (JoplinViewsEditors)](https://joplinapp.org/api/references/plugin_api/classes/JoplinViewsEditors.html) [2] [Joplin getting started with plugins](https://joplinapp.org/help/api/get_started/plugins)