Skip to content

cmd

Go
import "github.com/andreoliwa/logseq-doctor/cmd"

Index

Constants

Go
const (
    defaultServePort     = 8091
    defaultPocketBaseURL = "http://127.0.0.1:8090"
    pbReadyTimeout       = 10 * time.Second
    shutdownTimeout      = 5 * time.Second
    readHeaderTimeout    = 5 * time.Second
)

firstLineCount is the split limit used to extract the first line of block content.

Go
const firstLineCount = 2

Go
const groomDefaultLimit = 10

Go
const groomDefaultOlderThan = "1 year"

Go
const groomDefaultTermWidth = 80

Go
const groomFetchMultiplier = 5 // fetch 5× the limit to absorb tasks filtered out by HasFutureDate

Go
const groomNameMargin = 2 // leading space + right margin

Go
const groomSep = "────────────────────────────────────────────"

Variables

ErrDestinationExists is returned when the destination file already exists during --move-to.

Go
var ErrDestinationExists = errors.New("destination already exists")

ErrMutuallyExclusive is returned when --in-place and --move-to are both set.

Go
var ErrMutuallyExclusive = errors.New("--move-to and --in-place cannot be used together")

Go
var backlogCSS []byte

Go
var backlogCmd = &cobra.Command{
    Use:   "backlog [partial page names]",
    Short: "Aggregate tasks from multiple pages into a backlog",
    Long: `The backlog command aggregates tasks from one or more pages into a unified backlog.

If partial page names are provided, only page titles that contain the provided names are processed.

Each line on the "backlog" page that includes references to other pages or tags generates a separate backlog.
The first page in the line determines the name of the backlog page.
Tasks are retrieved from all provided pages or tags.
This setup enables users to rearrange tasks using the arrow keys and manage task states (start/stop)
directly within the interface.`,
    Run: func(_ *cobra.Command, args []string) {
        path := os.Getenv("LOGSEQ_GRAPH_PATH")
        logseqAPI := logseqapi.NewLogseqAPI(path,
            os.Getenv("LOGSEQ_HOST_URL"), os.Getenv("LOGSEQ_API_TOKEN"))
        graph := logseqapi.OpenGraphFromPath(path)
        reader := backlog.NewPageConfigReader(graph, "backlog")
        proc := backlog.NewBacklog(graph, logseqAPI, reader, time.Now)

        err := proc.ProcessAll(args)
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
    },
}

Go
var backlogHTML []byte

contentCmd represents the content command.

Go
var contentCmd = &cobra.Command{
    Use:   "content",
    Short: "Append raw Markdown content to Logseq",
    Long: `Append raw Markdown content to Logseq.

Pipe your content via stdin.
For now, it will be appended at the end of the current journal page.`,
    Run: func(_ *cobra.Command, _ []string) {
        graph := api.OpenGraphFromPath(os.Getenv("LOGSEQ_GRAPH_PATH"))
        stdin := internal.ReadFromStdin()

        var targetDate time.Time

        if journalFlag != "" {
            parsedDate, err := time.Parse("2006-01-02", journalFlag)
            if err != nil {
                log.Fatalln("Invalid journal date format. Use YYYY-MM-DD:", err)
            }

            targetDate = parsedDate
        } else {
            targetDate = time.Now()
        }

        _, err := internal.AppendRawMarkdownToJournal(graph, targetDate, stdin)
        if err != nil {
            log.Fatalln(err)
        }
    },
}

Go
var dashboardCmd = &cobra.Command{
    Use:     "dashboard",
    Aliases: DashboardAliases(),
    Short:   "Start PocketBase and the backlog web UI",
    Long: `Starts PocketBase as a managed subprocess, waits for it to be ready,
then serves the backlog dashboard at http://localhost:8091 (configurable).

Environment variables:
  POCKETBASE_URL       PocketBase URL (default http://127.0.0.1:8090)
  POCKETBASE_USERNAME  PocketBase admin email
  POCKETBASE_PASSWORD  PocketBase admin password
  LOGSEQ_GRAPH_PATH    Path to Logseq graph (required for write-back)
  LQD_SERVE_PORT       HTTP server port (default 8091)`,
    RunE: runDashboard,
}

errGroomNoCollection is returned when the lqd_tasks collection does not exist in PocketBase.

Go
var errGroomNoCollection = errors.New("no tasks found. Run 'lqd sync --init' first")

groomStyles holds the lipgloss styles for the groom TUI.

Go
var groomStyles = struct {
    separator lipgloss.Style
    header    lipgloss.Style
    taskName  lipgloss.Style
    label     lipgloss.Style
    value     lipgloss.Style
    age       lipgloss.Style
    actions   lipgloss.Style
    prompt    lipgloss.Style
    success   lipgloss.Style
    warning   lipgloss.Style
    errStyle  lipgloss.Style
    link      lipgloss.Style
}{
    separator: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
    header:    lipgloss.NewStyle().Foreground(lipgloss.Color("33")).Bold(true),
    taskName:  lipgloss.NewStyle().Foreground(lipgloss.Color("255")).Bold(true),
    label:     lipgloss.NewStyle().Foreground(lipgloss.Color("245")),
    value:     lipgloss.NewStyle().Foreground(lipgloss.Color("252")),
    age:       lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Italic(true),
    actions:   lipgloss.NewStyle().Foreground(lipgloss.Color("255")).Bold(true),
    prompt:    lipgloss.NewStyle().Foreground(lipgloss.Color("33")).Bold(true),
    success:   lipgloss.NewStyle().Foreground(lipgloss.Color("82")),
    warning:   lipgloss.NewStyle().Foreground(lipgloss.Color("226")),
    errStyle:  lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Bold(true),
    link:      lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Underline(true),
}

Go
var journalFlag string //nolint:gochecknoglobals

mdCmd represents the md command using the default dependencies.

Go
var mdCmd = NewMdCmd(nil) //nolint:gochecknoglobals

rootCmd represents the base command when called without any subcommands.

Go
var rootCmd = &cobra.Command{
    Use:   "lqd",
    Short: "Logseq Doctor heals your Markdown files for Logseq",
    Long: `Logseq Doctor heals your Markdown files for Logseq.

Convert flat Markdown to Logseq outline, clean up Markdown,
prevent invalid content, and more stuff to come.`,
}

taskCmd represents the task command using the default dependencies.

Go
var taskCmd = NewTaskCmd() //nolint:gochecknoglobals

tidyUpCmd represents the tidyUp command.

Go
var tidyUpCmd = &cobra.Command{
    Use:   "tidy-up file1.md [file2.md ...]",
    Short: "Tidy up your Markdown files.",

    Long: `Tidy up your Markdown files, checking for invalid content and fixing some of them automatically.

- Check for forbidden references to pages/tags
- Check for running tasks (DOING)
- Check for double spaces`,
    Args: cobra.MinimumNArgs(1),
    Run: func(_ *cobra.Command, args []string) {
        graph := api.OpenGraphFromPath(os.Getenv("LOGSEQ_GRAPH_PATH"))

        exitCode := 0

        for _, path := range args {
            if internal.TidyUpOneFile(graph, path) != 0 {
                exitCode = 1
            }
        }

        os.Exit(exitCode)
    },
}

func BuildHTTPMux

Go
func BuildHTTPMux(pbURL, token, graphPath string) *http.ServeMux

BuildHTTPMux creates the HTTP mux with all routes registered.

func DashboardAliases

Go
func DashboardAliases() []string

DashboardAliases returns the Cobra aliases for the dashboard command. Exported so tests can verify the alias without accessing the unexported global.

func Execute

Go
func Execute()

Execute adds all child commands to the root command and sets flags appropriately. This is called by main.main(). It only needs to happen once to the rootCmd.

func NewGroomCmd

Go
func NewGroomCmd(deps *GroomDependencies) *cobra.Command

NewGroomCmd creates a new groom command with the specified dependencies. If deps is nil, it uses default (production) implementations.

func NewMdCmd

Go
func NewMdCmd(deps *MdDependencies) *cobra.Command

NewMdCmd creates a new md command with the specified dependencies. If deps is nil, it uses default implementations.

func NewOutlineCmd

Go
func NewOutlineCmd(deps *OutlineDependencies) *cobra.Command

NewOutlineCmd creates the outline command with injectable dependencies. If deps is nil, production defaults are used. Individual nil fields also fall back to their defaults, so tests can inject only the fields they need.

func NewSyncCmd

Go
func NewSyncCmd(deps *SyncDependencies) *cobra.Command

NewSyncCmd creates a new sync command with the specified dependencies. If deps is nil, it uses default (production) implementations.

func NewTaskAddCmd

Go
func NewTaskAddCmd(deps *TaskAddDependencies) *cobra.Command

NewTaskAddCmd creates a new task add subcommand with the specified dependencies. If deps is nil, it uses default implementations.

func NewTaskCmd

Go
func NewTaskCmd() *cobra.Command

NewTaskCmd creates the parent task command.

func NewTaskLsCmd

Go
func NewTaskLsCmd(deps *TaskLsDependencies) *cobra.Command

NewTaskLsCmd creates a new task ls subcommand with the specified dependencies. If deps is nil, it uses default implementations. Individual nil fields also fall back to their defaults, so a test can inject only LogseqAPI + Out and leave GraphName defaulted.

func ParseDateFromJournalFlag

Go
func ParseDateFromJournalFlag(journalFlag string, timeNow func() time.Time) (time.Time, error)

ParseDateFromJournalFlag parses the journal flag and returns the target date. If journalFlag is empty, it returns the current time from timeNow. If journalFlag is not empty, it parses it as YYYY-MM-DD format. Returns an error if the date format is invalid.

func ResolveEnvWithDefault

Go
func ResolveEnvWithDefault(key, fallback string) string

ResolveEnvWithDefault returns the env var value or fallback if unset.

func ResolvePort

Go
func ResolvePort(cmd *cobra.Command) int

ResolvePort returns the effective port: flag > env var > default.

func addJournalFlag

Go
func addJournalFlag(cmd *cobra.Command, flagVar *string)

addJournalFlag adds a --journal/-j flag to the command.

func addKeyFlag

Go
func addKeyFlag(cmd *cobra.Command, flagVar *string)

addKeyFlag adds a --key/-k flag to the command.

func addPageFlag

Go
func addPageFlag(cmd *cobra.Command, flagVar *string, what string)

addPageFlag adds a --page/-p flag to the command with customizable help text.

func addParentFlag

Go
func addParentFlag(cmd *cobra.Command, flagVar *string, what string)

addParentFlag adds a --parent flag to the command with customizable help text.

func applyChanges

Go
func applyChanges(pbClient *pocketbase.Client, desired []map[string]any) error

func authenticate

Go
func authenticate(pbURL, pbUser, pbPass string) (string, error)

authenticate obtains a PocketBase token. Returns "" and nil when credentials are absent.

func backlogUnrankedSectionTexts

Go
func backlogUnrankedSectionTexts() []string

unrankedSectionTexts lists the header texts that mark the start of an unranked section on a backlog page. Any block ref that is a child of one of these headers backlogUnrankedSectionTexts returns the header texts that mark the start of an unranked section on a backlog page. Any block ref that is a child of one of these headers is assigned SectionUnranked during sync.

func buildDesiredRecords

Go
func buildDesiredRecords(tasks []logseqapi.TaskJSON, ranks map[string][]lqdsync.RankInfo, tagsByUUID map[string]string, currentTime func() time.Time) []map[string]any

func collectBacklogRefs

Go
func collectBacklogRefs(graph *logseq.Graph, config *backlog.Config) (map[string][]lqdsync.RankInfo, []string)

collectBacklogRefs scans the Focus page and all configured backlog pages, returning a rank map (uuid → []RankInfo) and the ordered list of backlog names. Each ref is classified as SectionRanked or SectionUnranked based on which section header it lives under on the page.

func collectFocusRefs

Go
func collectFocusRefs(graph *logseq.Graph, focusPagePath string, ranks map[string][]lqdsync.RankInfo, backlogOrder *[]string)

func collectPageRefs

Go
func collectPageRefs(graph *logseq.Graph, backlogPagePath string, ranks map[string][]lqdsync.RankInfo, backlogOrder *[]string)

func fetchGroomTasks

Go
func fetchGroomTasks(now, thresholdDate time.Time, limit int) (*pocketbase.Client, []map[string]any, error)

fetchGroomTasks initialises PocketBase, checks the collection, and fetches matching tasks. Returns (nil, nil, nil) with a printed message when there are no tasks.

func fetchLogseqTasks

Go
func fetchLogseqTasks(logseqAPI logseqapi.LogseqAPI) ([]logseqapi.TaskJSON, error)

func fillOutlineDeps

Go
func fillOutlineDeps(deps *OutlineDependencies)

func gracefulShutdown

Go
func gracefulShutdown(srv *http.Server) error

gracefulShutdown shuts the server down with a fresh timeout context. The parent context is already cancelled at this point, so a new one is needed.

func groomHandleTask

Go
func groomHandleTask(graph *logseq.Graph, api api.LogseqAPI, backlogConfig *backlog.Config, pbUpdater func(recordID string, groomedAt time.Time) error, task map[string]any, now time.Time, counts *groom.Counts) bool

groomHandleTask reads keypresses for a single task until a valid action is taken. Returns true if the user requested quit.

func groomPrintApplyError

Go
func groomPrintApplyError(applyErr error, counts *groom.Counts)

func groomSyncPocketBase

Go
func groomSyncPocketBase(pbUpdater func(recordID string, groomedAt time.Time) error, action *groom.Action, task map[string]any, now time.Time)

func handleConfig

Go
func handleConfig(writer http.ResponseWriter, graphPath string)

handleConfig returns UI configuration derived from the server environment.

func handleMoveToUnranked

Go
func handleMoveToUnranked(writer http.ResponseWriter, req *http.Request, graphPath, pbURL, token string)

handleMoveToUnranked handles POST /internal/move-to-unranked.

func hasRankInBacklog

Go
func hasRankInBacklog(rankInfos []lqdsync.RankInfo, backlogName string) bool

hasRankInBacklog reports whether the given backlog name already has an entry in the rank slice.

func init

Go
func init()

func initCollection

Go
func initCollection(client *pocketbase.Client) error

func logseqGraphName

Go
func logseqGraphName() string

logseqGraphName extracts the graph name from the graph path for deep links.

func openGroomResources

Go
func openGroomResources() (*logseq.Graph, api.LogseqAPI, *backlog.Config, error)

openGroomResources opens the Logseq graph, API, and reads the backlog config.

func printTaskCard

Go
func printTaskCard(task map[string]any, index, total int, now time.Time, termWidth int)

printTaskCard prints a single task card to stdout.

func processGroomTasks

Go
func processGroomTasks(tasks []map[string]any, now time.Time, graph *logseq.Graph, api api.LogseqAPI, backlogConfig *backlog.Config, pbUpdater func(recordID string, groomedAt time.Time) error) groom.Counts

processGroomTasks presents tasks one at a time in a plain scrolling terminal loop. Each task card is printed, the user presses a key, the result is printed, then the next task scrolls into view. No alternate screen — every action is permanently visible.

func readKey

Go
func readKey() (string, error)

readKey reads a single keypress from /dev/tty without requiring Enter.

func resolveBacklogPage

Go
func resolveBacklogPage(graphPath, shortName string) string

resolveBacklogPage maps a short backlog name (e.g. "self") to its full page title (e.g. "Backlogs/self") by reading the backlog config page from the graph. Falls back to the short name if the config cannot be read or the name is not found.

func runDashboard

Go
func runDashboard(cmd *cobra.Command, _ []string) error

func runGroomWith

Go
func runGroomWith(now time.Time, olderThan string, limit int)

runGroomWith is the testable core of runGroom.

func runOutline

Go
func runOutline(deps *OutlineDependencies, args []string, inPlace bool, moveTo string, keepBreaks bool) error

runOutline is the core logic for the outline command.

func runOutlineInPlace

Go
func runOutlineInPlace(deps *OutlineDependencies, path string, opts internal.OutlineOptions) error

runOutlineInPlace converts a file and writes the result back to the same path.

func runOutlineMoveTo

Go
func runOutlineMoveTo(deps *OutlineDependencies, path, moveTo string, opts internal.OutlineOptions) error

runOutlineMoveTo converts a file and writes the result to a destination directory, then removes the source.

func runOutlineToStdout

Go
func runOutlineToStdout(deps *OutlineDependencies, path string, opts internal.OutlineOptions) error

runOutline handles the outline command's stdout-only branch (no flags).

func runSyncPipeline

Go
func runSyncPipeline(graph *logseq.Graph, logseqAPI logseqapi.LogseqAPI, pbClient *pocketbase.Client, currentTime func() time.Time) error

func runSyncWith

Go
func runSyncWith(currentTime func() time.Time, initFlag bool)

runSyncWith is the testable core of runSync.

func runTaskLs

Go
func runTaskLs(deps *TaskLsDependencies, flags *taskLsFlags, args []string) error

func startHTTPServer

Go
func startHTTPServer(ctx context.Context, port int, mux http.Handler) error

startHTTPServer runs the server until a signal arrives or a listen error occurs.

func terminalWidth

Go
func terminalWidth() int

terminalWidth returns the current terminal width, falling back to groomDefaultTermWidth.

func updateGroomCounts

Go
func updateGroomCounts(counts *groom.Counts, actionName string)

updateGroomCounts increments the appropriate counter for the given action name.

type GroomDependencies

GroomDependencies holds all injectable dependencies for the groom command. This enables unit testing without connecting to PocketBase, Logseq, or a terminal.

Go
type GroomDependencies struct {
    TimeNow func() time.Time
}

type MdDependencies

MdDependencies holds all the dependencies for the md command.

Go
type MdDependencies struct {
    InsertFn  func(*internal.InsertMarkdownOptions) error
    OpenGraph func(string) *logseq.Graph
    ReadStdin func() string
    TimeNow   func() time.Time
}

type OutlineDependencies

OutlineDependencies holds injectable dependencies for the outline command.

Go
type OutlineDependencies struct {
    Convert   func(input string, opts internal.OutlineOptions) string
    ReadFile  func(path string) (string, error)
    WriteFile func(path string, data string) error
    Stat      func(path string) (os.FileInfo, error)
    Rename    func(oldpath, newpath string) error
    Remove    func(path string) error
    Stdin     func() string
    Out       io.Writer
}

type SyncDependencies

SyncDependencies holds all injectable dependencies for the sync command. This enables unit testing without connecting to PocketBase or Logseq.

Go
type SyncDependencies struct {
    TimeNow func() time.Time
}

type TaskAddDependencies

TaskAddDependencies holds all the dependencies for the task add command.

Go
type TaskAddDependencies struct {
    AddTaskFn func(*logseqext.AddTaskOptions) error
    OpenGraph func(string) *logseq.Graph
    TimeNow   func() time.Time
}

type TaskLsDependencies

TaskLsDependencies holds all the dependencies for the task ls command.

Go
type TaskLsDependencies struct {
    NewAPI    func() api.LogseqAPI
    GraphName func() string
    Out       io.Writer
}

type taskLsFlags

taskLsFlags holds the flag values for NewTaskLsCmd.

Go
type taskLsFlags struct {
    canceled  bool
    done      bool
    completed bool
    json      bool
    verbose   bool
}

internal

Go
import "github.com/andreoliwa/logseq-doctor/internal"

Index

Constants

Go
const (
    outlineIndentUnit      = "\t" // one tab per indent level, matching Logseq's native format
    outlineBullet          = "-"
    outlineOrderedListProp = "logseq.order-list-type:: number"
    // outlinePropertyPrefix is the indent for a block property line: tab(s) + 2 spaces.
    outlinePropertySpaces = "  "
)

Variables

ErrPageIsNil is returned when a page is nil.

Go
var ErrPageIsNil = errors.New("page is nil")

PageColor is a color function for page names.

Go
var PageColor = color.New(color.FgHiWhite).SprintfFunc() //nolint:gochecknoglobals

func AppendRawMarkdownToJournal

Go
func AppendRawMarkdownToJournal(graph *logseq.Graph, date time.Time, rawMarkdown string) (int, error)

AppendRawMarkdownToJournal appends raw Markdown content to the journal page for the given date. I tried appending blocks with `logseq-go` but there is and with text containing brackets. e.g. "[something]" is escaped like "\[something\]" and this breaks links.

func FlatMarkdownToOutline

Go
func FlatMarkdownToOutline(input string, opts OutlineOptions) string

FlatMarkdownToOutline converts flat Markdown to a Logseq bullet outline. It strips YAML frontmatter before parsing and prepends it unchanged to the result. It is idempotent: if the conversion is a no-op, the original input is returned.

func InsertMarkdown

Go
func InsertMarkdown(opts *InsertMarkdownOptions) error

InsertMarkdown inserts Markdown content to a page or journal. If Page is provided, adds to that page. Otherwise, adds to journal for Date. If Key is provided, it searches for an existing block containing that key (case-insensitive) and updates it. Otherwise, creates a new block. If ParentText is provided, it searches for the first block containing that text and inserts the content as a child block. Otherwise, appends to the end.

func IsAncestor

Go
func IsAncestor(block, ancestor *content.Block) bool

IsAncestor checks if ancestor is an ancestor of block by traversing up the parent chain. Returns true if ancestor is found in the parent hierarchy of block, false otherwise. Returns true if block and ancestor are the same block.

func IsValidMarkdownFile

Go
func IsValidMarkdownFile(filePath string) bool

IsValidMarkdownFile checks if a file is a Markdown file, by looking at its extension, not its content.

func ReadFromStdin

Go
func ReadFromStdin() string

ReadFromStdin reads from stdin and returns the content as a string. It doesn't return an error and aborts the program if it fails because it's an internal function.

func SortAndRemoveDuplicates

Go
func SortAndRemoveDuplicates(elements []string) []string

func TidyUpOneFile

Go
func TidyUpOneFile(graph *logseq.Graph, path string) int

func addContent

Go
func addContent(page logseq.Page, parentBlock *content.Block, contentText string) error

addContent adds content either as a child block to the specified parent or as a top-level block to the page.

func convertOnce

Go
func convertOnce(body string, opts OutlineOptions) string

convertOnce parses and converts a Markdown body (without frontmatter) to outline form.

func findFirstChildBlock

Go
func findFirstChildBlock(block *content.Block) *content.Block

findFirstChildBlock finds the first child block in a block.

func findNestedList

Go
func findNestedList(item ast.Node) *ast.List

findNestedList returns the first *ast.List child of a list item, or nil.

func insertNewContentNodes

Go
func insertNewContentNodes(block *content.Block, parsedBlock *content.Block, firstChildBlock *content.Block)

insertNewContentNodes inserts new content nodes from parsedBlock into block.

func outlineLine

Go
func outlineLine(level int, lineText string) string

outlineLine formats a single outline bullet at the given nesting level.

func removeOldContentNodes

Go
func removeOldContentNodes(block *content.Block)

removeOldContentNodes removes content nodes from a block while preserving Properties, Logbook, and child Blocks.

func shouldPreserveNode

Go
func shouldPreserveNode(node content.Node) bool

shouldPreserveNode returns true if the node should be preserved (Properties, Logbook, or child Blocks).

func stripFrontmatter

Go
func stripFrontmatter(input string) (string, string)

stripFrontmatter splits YAML frontmatter from the body. If the input starts with "---\n" and contains a closing "\n---\n", the frontmatter (including fences) is returned separately from the body. This must run before goldmark sees the input so that "---" is not parsed as a thematic break or setext heading underline.

func updateExistingBlock

Go
func updateExistingBlock(block *content.Block, newContent string) error

updateExistingBlock updates an existing block's content while preserving children, properties, and logbook.

type ChangedContents

ChangedContents is the result of a check function that modifies file contents directly without a transaction.

Go
type ChangedContents struct {
    Msg         string
    NewContents string
}

func RemoveUnnecessaryBracketsFromTags

Go
func RemoveUnnecessaryBracketsFromTags(oldContents string) ChangedContents

RemoveUnnecessaryBracketsFromTags removes unnecessary brackets from hashtags. logseq-go rewrites tags correctly when saving the transaction, removing unnecessary brackets. But, when reading the file, the AST doesn't provide the information if a tag has brackets or not. So I would have to rewrite the file to fix them, and I don't want to do it every time there is a tag without spaces. Also, as of 2024-12-30, logseq-go has a bug when reading properties with spaces in values, which causes them to be partially removed from the file, destroying data. I will report it soon.

type ChangedPage

ChangedPage is the result of a check function that modifies Markdown through a Page and a transaction.

Go
type ChangedPage struct {
    Msg     string
    Changed bool
}

func CheckForbiddenReferences

Go
func CheckForbiddenReferences(page logseq.Page) ChangedPage

CheckForbiddenReferences checks if a page has forbidden references to other pages or tags.

func CheckRunningTasks

Go
func CheckRunningTasks(page logseq.Page) ChangedPage

CheckRunningTasks checks if a page has running tasks (DOING, etc.).

func RemoveDoubleSpaces

Go
func RemoveDoubleSpaces(page logseq.Page) ChangedPage

RemoveDoubleSpaces removes double spaces from text, page links, and tags, except for tables.

func RemoveEmptyBullets

Go
func RemoveEmptyBullets(page logseq.Page) ChangedPage

type InsertMarkdownOptions

InsertMarkdownOptions contains options for inserting Markdown content.

Go
type InsertMarkdownOptions struct {
    Graph      *logseq.Graph
    Date       time.Time
    Page       string // Page name to add content to (empty = journal)
    Content    string
    ParentText string // Partial text to search for in parent blocks
    Key        string // Unique key to search for existing block (case-insensitive)
}

type OutlineOptions

OutlineOptions configures the flat-Markdown-to-outline conversion.

Go
type OutlineOptions struct {
    // KeepBreaks preserves blank lines between blocks as empty "- " bullet lines.
    KeepBreaks bool
}

type converter

converter walks the goldmark AST and builds the outline string.

Go
type converter struct {
    source       []byte
    currentLevel int
    sb           strings.Builder
    keepBreaks   bool
}

func (*converter) collectInline

Go
func (c *converter) collectInline(node ast.Node, builder *strings.Builder)

collectInline recursively renders inline nodes into builder.

func (*converter) handleListItem

Go
func (c *converter) handleListItem(node ast.Node, isOrdered bool)

handleListItem emits the list item text and manages level for nested lists. isOrdered indicates whether the parent list is an ordered list; when true, the Logseq block property "logseq.order-list-type:: number" is written after the bullet as a plain indented line (no "- " prefix).

On re-parse the converted output uses unordered "-" bullets, so isOrdered is false. The property line appears as a soft-line-break continuation inside the TextBlock. We detect it directly in itemText to preserve it on re-parse (idempotency).

func (*converter) inlineText

Go
func (c *converter) inlineText(node ast.Node) string

inlineText collects the rendered text of all inline children of a node. It replicates the Python LogseqRenderer.render_inner / render_link behavior.

func (*converter) listItemText

Go
func (c *converter) listItemText(item ast.Node) string

listItemText extracts the text content from a list item's first paragraph or inline block.

func (*converter) walk

Go
func (c *converter) walk(node ast.Node, entering bool) (ast.WalkStatus, error)

walk is the goldmark AST visitor function.

func (*converter) walkNestedList

Go
func (c *converter) walkNestedList(list *ast.List)

walkNestedList recursively processes a nested list and its items.

lqd

Go
import "github.com/andreoliwa/logseq-doctor/cmd/lqd"

Copyright © 2024 W Augusto Andreoli \<andreoliwa@sent.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Index

func main

Go
func main()

api

Go
import "github.com/andreoliwa/logseq-doctor/internal/api"

Package api provides the Logseq HTTP API client and graph-opening utilities.

Index

Variables

ErrBlockNotFoundViaAPI is returned when a block UUID query returns no results from the Logseq API.

Go
var ErrBlockNotFoundViaAPI = errors.New("block not found via API")

ErrBlockNotOnDiskAfterWriteback is returned when a block is still missing from disk after a write-back attempt.

Go
var ErrBlockNotOnDiskAfterWriteback = errors.New("block still not found on disk after write-back")

ErrFailedOpenGraph is returned when the graph cannot be opened.

Go
var ErrFailedOpenGraph = errors.New("failed to open graph")

ErrInvalidResponseStatus is returned when the Logseq API returns a non-200 status code.

Go
var ErrInvalidResponseStatus = errors.New("invalid response status")

ErrMissingConfig is returned when the Logseq API token or host URL is not set.

Go
var ErrMissingConfig = errors.New("LOGSEQ_API_TOKEN and LOGSEQ_HOST_URL must be set")

func BuildRefLookup

Go
func BuildRefLookup(tasks []TaskJSON) map[int]string

BuildRefLookup builds a mapping from Logseq ref ID to human-readable name.

func BuildTaskListQuery

Go
func BuildTaskListQuery(tags []string, includeCanceled, includeDone bool) string

BuildTaskListQuery assembles the Logseq Datalog query for listing tasks. It matches the Python `lqdpy tasks` query format exactly.

func EnrichTasksWithAncestorTags

Go
func EnrichTasksWithAncestorTags(tasks []TaskJSON, refLookup map[int]string) map[string]string

EnrichTasksWithAncestorTags adds inherited tags from pathRefs to each task's tag set.

func FindBlockOnDisk

Go
func FindBlockOnDisk(graph *logseq.Graph, api LogseqAPI, uuid string) (*content.Block, *logseq.Transaction, error)

FindBlockOnDisk locates a task block in the graph by querying the Logseq API for its page, then opening that page and finding the block by its id:: property. If the block is not on disk yet and the API is available, it forces a UUID write-back. This is a future candidate to move into logseq-go once the library supports graph queries directly.

func OpenGraphFromPath

Go
func OpenGraphFromPath(path string) *logseq.Graph

OpenGraphFromPath opens a Logseq graph from the given directory path. Delegates to logseqext.OpenGraphFromPath.

func OpenPage

Go
func OpenPage(graph *logseq.Graph, pageTitle string) logseq.Page

OpenPage opens a page in the Logseq graph. Delegates to logseqext.OpenPage.

func OpenPageForBlock

Go
func OpenPageForBlock(transaction *logseq.Transaction, blockInfo *BlockQueryInfo) (logseq.Page, error)

OpenPageForBlock opens the appropriate page (journal or regular) for a block described by blockInfo.

func SortTasksByDate

Go
func SortTasksByDate(tasks []TaskJSON)

SortTasksByDate sorts tasks in place by (JournalDay, Content) ascending, matching Python's Block.sort_by_date behavior.

func TaskDoing

Go
func TaskDoing(t TaskJSON) bool

TaskDoing checks if the task has the DOING marker.

func TaskFutureScheduled

Go
func TaskFutureScheduled(t TaskJSON, currentTime func() time.Time) bool

TaskFutureScheduled checks if the task is scheduled for the future (tomorrow onwards) and it's not overdue.

func TaskOverdue

Go
func TaskOverdue(t TaskJSON, currentTime func() time.Time) bool

TaskOverdue checks if the task is overdue based on deadline or scheduled date.

func buildDirectRefIDSet

Go
func buildDirectRefIDSet(task TaskJSON) map[int]bool

buildDirectRefIDSet creates a set of ref IDs that are direct references (including page).

func collectAncestorTags

Go
func collectAncestorTags(task TaskJSON, directRefIDs map[int]bool, refLookup map[int]string) []string

collectAncestorTags gathers tags from pathRefs that are not in the direct ref set.

func collectRefCandidates

Go
func collectRefCandidates(tasks []TaskJSON, refLookup map[int]string) map[int]map[string]int

collectRefCandidates gathers tag candidates for unresolved ref IDs.

func openBlockFromInfo

Go
func openBlockFromInfo(graph *logseq.Graph, blockInfo *BlockQueryInfo, uuid string) (*content.Block, *logseq.Transaction, error)

openBlockFromInfo opens the page described by blockInfo and finds the block by its id:: property.

func populatePageRef

Go
func populatePageRef(refLookup map[int]string, task TaskJSON)

populatePageRef adds the page reference for a task to the lookup.

func resolveRefCandidates

Go
func resolveRefCandidates(refLookup map[int]string, candidates map[int]map[string]int)

resolveRefCandidates picks the best tag name for each unresolved ref ID.

type BlockQueryInfo

BlockQueryInfo holds the result of a UUID lookup via Logseq API.

Go
type BlockQueryInfo struct {
    PageName    string
    JournalDate time.Time
    IsJournal   bool
}

func FindBlockByUUID

Go
func FindBlockByUUID(api LogseqAPI, uuid string) (*BlockQueryInfo, error)

FindBlockByUUID queries the Logseq HTTP API to find a block by UUID. Uses PostDatascriptQuery (logseq.db.datascriptQuery) because the pull syntax required here is not supported by logseq.db.q (PostQuery). The nested {:block/page [*]} expands page attributes; without it, page is just {id: N}.

func extractBlockInfo

Go
func extractBlockInfo(page map[string]any) *BlockQueryInfo

extractBlockInfo extracts page name and journal info from a page map. Logseq's datascript API returns hyphenated keys: "journal-day", "original-name".

func parseBlockQueryResponse

Go
func parseBlockQueryResponse(jsonStr, uuid string) (*BlockQueryInfo, error)

parseBlockQueryResponse parses the JSON response from a block UUID query.

type CategorizedTasks

CategorizedTasks holds sets of task UUIDs grouped by category.

Go
type CategorizedTasks struct {
    All             *set.Set[TaskUUID]
    Overdue         *set.Set[TaskUUID]
    Doing           *set.Set[TaskUUID]
    FutureScheduled *set.Set[TaskUUID]
    TaskLookup      map[TaskUUID]TaskJSON
}

func NewCategorizedTasks

Go
func NewCategorizedTasks() CategorizedTasks

NewCategorizedTasks creates a new CategorizedTasks with initialized sets.

type LogseqAPI

LogseqAPI is the interface for communicating with a running Logseq instance via its HTTP API.

Go
type LogseqAPI interface {
    PostQuery(query string) (string, error)
    PostDatascriptQuery(query string) (string, error)
    // UpsertBlockProperty sets a block property via the Logseq Editor API.
    // This causes Logseq to write the property to the .md file immediately,
    // which is useful to force the id:: property onto disk for blocks that
    // Logseq has tracked internally but not yet written back.
    UpsertBlockProperty(uuid, key, value string) error
}

func NewLogseqAPI

Go
func NewLogseqAPI(path, hostURL, apiToken string) LogseqAPI

NewLogseqAPI creates a new LogseqAPI instance.

type PageJSON

PageJSON holds page-level metadata from the Logseq API.

Go
type PageJSON struct {
    ID           int    `json:"id"`
    JournalDay   int    `json:"journalDay"`
    Name         string `json:"name"`
    OriginalName string `json:"originalName"`
}

type RefJSON

RefJSON represents a reference entry from the Logseq API response.

Go
type RefJSON struct {
    ID   int    `json:"id"`
    Name string `json:"name,omitempty"`
}

type TaskJSON

TaskJSON represents a task block from the Logseq HTTP API.

Go
type TaskJSON struct {
    UUID                 TaskUUID          `json:"uuid"`
    Marker               string            `json:"marker"`
    Content              string            `json:"content"`
    Page                 PageJSON          `json:"page"`
    Deadline             int               `json:"deadline"`
    Scheduled            int               `json:"scheduled"`
    Refs                 []RefJSON         `json:"refs"`
    PathRefs             []RefJSON         `json:"pathRefs"`
    PropertiesTextValues map[string]string `json:"propertiesTextValues"`
}

func ExtractTasksFromJSON

Go
func ExtractTasksFromJSON(jsonStr string) ([]TaskJSON, error)

ExtractTasksFromJSON parses a JSON string into a slice of TaskJSON.

type TaskUUID

TaskUUID is a type alias for task block UUIDs, making it clear when a string represents a Logseq block UUID.

Go
type TaskUUID = string

type logseqAPIImpl

Go
type logseqAPIImpl struct {
    path     string
    hostURL  string
    apiToken string
}

func (*logseqAPIImpl) PostDatascriptQuery

Go
func (l *logseqAPIImpl) PostDatascriptQuery(query string) (string, error)

PostDatascriptQuery sends a Datascript query ([:find ...]) to the Logseq API. Use this instead of PostQuery for queries that require pull syntax or complex patterns.

func (*logseqAPIImpl) PostQuery

Go
func (l *logseqAPIImpl) PostQuery(query string) (string, error)

PostQuery sends a query to the Logseq API and returns the result as JSON.

func (*logseqAPIImpl) UpsertBlockProperty

Go
func (l *logseqAPIImpl) UpsertBlockProperty(uuid, key, value string) error

UpsertBlockProperty calls logseq.Editor.upsertBlockProperty to set a property on a block. Unlike PostQuery/PostDatascriptQuery which take a single query string, this method passes three separate args: uuid, key, value. Logseq then writes the property to the .md file, which forces the id:: property onto disk for blocks that haven't been persisted yet. Logseq lazy-writes block UUIDs: they exist in its DB but only hit .md files when something triggers a write (a backlink, an edit, or this Editor API call).

func (*logseqAPIImpl) postAPI

Go
func (l *logseqAPIImpl) postAPI(method, query string) (string, error)

postAPI is the shared HTTP implementation for PostQuery and PostDatascriptQuery.

backlog

Go
import "github.com/andreoliwa/logseq-doctor/internal/backlog"

Index

Constants

Section values for the PocketBase `section` field. Ranked=1, Unranked=2, Orphan=3 so that (backlog_index, section, rank) sorts ranked tasks before unranked tasks before orphans within any backlog.

Go
const (
    SectionRanked   = 1 // manually ordered, above the ⤵️ Unranked tasks divider
    SectionUnranked = 2 // under ⤵️ Unranked tasks, 📅 Overdue tasks, ⏰ Scheduled tasks, ✨ New tasks, 🏷️ Triaged tasks
    SectionOrphan   = 3 // not referenced in any backlog page
)

quickCapturePageName is the Logseq page linked in newly created section dividers so the user can identify dividers inserted by lqd backlog.

Go
const quickCapturePageName = "quick capture"

Variables

Backlog section header definitions. Detection uses Header.Matches (case-insensitive label search). Creation uses Header.String() so the canonical emoji+label+tasks is always written.

Go
var (
    HeaderFocus     = Header{"🎯", "Focus"}
    HeaderOverdue   = Header{"📅", "Overdue"}
    HeaderNewTasks  = Header{"✨", "New"}
    HeaderTriaged   = Header{"🏷️", "Triaged"}
    HeaderScheduled = Header{"⏰", "Scheduled"}
    HeaderUnranked  = Header{"⤵️", "Unranked"}
)

allHeaders is the full list used to normalize section dividers on write-back.

Go
var allHeaders = []Header{
    HeaderFocus, HeaderOverdue, HeaderNewTasks,
    HeaderTriaged, HeaderScheduled, HeaderUnranked,
}

focusSectionHeaders are the section dividers on the Focus page. Used to find the insertion point for new block refs.

Go
var focusSectionHeaders = []Header{HeaderOverdue, HeaderNewTasks, HeaderTriaged, HeaderScheduled}

regularAreaSectionHeaders are section headers that mark the boundary of the regular area. A top-level block matching any of these is a section divider, not part of the regular area.

Go
var regularAreaSectionHeaders = []Header{
    HeaderFocus, HeaderOverdue, HeaderNewTasks, HeaderTriaged, HeaderScheduled, HeaderUnranked,
}

func AddBlockRefToFocusPage

Go
func AddBlockRefToFocusPage(transaction *logseq.Transaction, focusPageTitle, uuid string) error

AddBlockRefToFocusPage adds a block ref ((uuid)) to the Focus page.

func BlockRefExistsUnder

Go
func BlockRefExistsUnder(parent *content.Block, uuid logseqapi.TaskUUID) bool

BlockRefExistsUnder returns true if a block ref with the given UUID exists anywhere in the descendant tree of parent.

func FindFirstSectionDivider

Go
func FindFirstSectionDivider(page logseq.Page) *content.Block

FindFirstSectionDivider finds the first block whose text matches a known section header.

func FormatCount

Go
func FormatCount(count int, singular, plural string) string

FormatCount returns a string with the count and the singular or plural form of a word.

func MoveBlockRefToTriagedSection

Go
func MoveBlockRefToTriagedSection(transaction *logseq.Transaction, backlogPage string, uuid logseqapi.TaskUUID, triagedText, scheduledText string) error

MoveBlockRefToTriagedSection moves a block ref to the Triaged section of a backlog page. If the ref exists in the regular area (not under Focus, New tasks, Overdue, or Scheduled), it is removed from there. If it's already in Triaged, no duplicate is added. Creates the Triaged section if it doesn't exist.

func NormalizeHeaderText

Go
func NormalizeHeaderText(page logseq.Page) bool

NormalizeHeaderText scans all top-level blocks on the page and normalizes any block whose text node contains a known header keyword:

  • fixes the text to the canonical "emoji Label tasks" form
  • upgrades a plain Paragraph container to a Heading level 1

Returns true if any block was changed.

func RemoveBlockRefFromRegularArea

Go
func RemoveBlockRefFromRegularArea(page logseq.Page, uuid logseqapi.TaskUUID)

RemoveBlockRefFromRegularArea removes the block ref with the given UUID from the regular area of the page. The regular area is any top-level block ref that is not itself a section divider. Section dividers (Focus, Overdue, New tasks, Triaged, Scheduled, Unranked) and their children are not part of the regular area. Since we walk only top-level blocks, child refs are never seen.

func addTasksToCategories

Go
func addTasksToCategories(jsonTasks []logseqapi.TaskJSON, tasks *logseqapi.CategorizedTasks, currentTime func() time.Time)

addTasksToCategories adds tasks to the appropriate categories in CategorizedTasks.

func applyDirectiveGroup

Go
func applyDirectiveGroup(graph *logseq.Graph, logseqAPI logseqapi.LogseqAPI, items []*blockDirective, currentTime func() time.Time) error

func applyDirectiveGroupAndCleanup

Go
func applyDirectiveGroupAndCleanup(graph *logseq.Graph, logseqAPI logseqapi.LogseqAPI, grp *directiveGroup, currentTime func() time.Time) bool

applyDirectiveGroupAndCleanup applies all directives in a group and strips their nodes. Returns true if the group was successfully applied.

func applyDirectiveToBlock

Go
func applyDirectiveToBlock(block *content.Block, directive *blockDirective, currentTime func() time.Time) error

func applyDirectives

Go
func applyDirectives(graph *logseq.Graph, logseqAPI logseqapi.LogseqAPI, directives []blockDirective, currentTime func() time.Time) bool

applyDirectives processes all collected directives: modifies the real task block on disk, then strips the directive node from the backlog page.

Directives for the same UUID are grouped and applied in a single transaction so the task file is opened and saved only once (e.g. WAITING + [#B] on the same block ref).

If a block is not on disk and the Logseq API is available, it forces a UUID write-back. If the API is unavailable, it warns and skips. Returns true if any directive was successfully applied (meaning the backlog page AST was mutated and the caller must save the backlog transaction).

func blockRefsFromPages

Go
func blockRefsFromPages(page logseq.Page) *set.Set[string]

func categorizeBlockRef

Go
func categorizeBlockRef(node content.Node, blockRef *content.BlockRef, block *content.Block, state *pageState, underScheduled bool, obsoleteBlockRefs, overdueBlockRefs, futureScheduledBlockRefs *set.Set[string], underTriaged bool) bool

categorizeBlockRef determines whether a block ref should be deleted based on its category.

func collectScheduledRefs

Go
func collectScheduledRefs(page logseq.Page, state *pageState)

collectScheduledRefs performs a first pass to find the Scheduled section divider and record all block-ref UUIDs that are descendants of it.

func collectTriagedRefs

Go
func collectTriagedRefs(page logseq.Page, state *pageState)

collectTriagedRefs performs a first pass over the page to find the Triaged section divider and record all block-ref UUIDs that are descendants of it.

func createTriagedSectionWithRef

Go
func createTriagedSectionWithRef(page logseq.Page, uuid logseqapi.TaskUUID, triagedText, scheduledText string) error

createTriagedSectionWithRef creates a new Triaged section with a block reference. It inserts the section before the Scheduled section if found, or appends to the end.

func defaultQuery

Go
func defaultQuery(pageTitle string) string

func handleBlockRefGuards

Go
func handleBlockRefGuards(blockRef *content.BlockRef, state *pageState, underTriaged, underUnranked, underScheduled bool, obsoleteBlockRefs *set.Set[string]) bool

handleBlockRefGuards applies early-exit guards for block refs before categorization. Returns true if the caller should return immediately (the ref was handled or preserved as-is).

func handleDefaultBlockRef

Go
func handleDefaultBlockRef(node content.Node, blockRef *content.BlockRef, state *pageState, underScheduled bool, futureScheduledBlockRefs *set.Set[string]) bool

handleDefaultBlockRef handles the default case in processBlockRef: moves stale Scheduled tasks back to new, and unpins regular tasks. Returns true if the block should be deleted.

func insertNewTasks

Go
func insertNewTasks(page logseq.Page, state *pageState, newBlockRefs, overdueBlockRefs, futureScheduledBlockRefs *set.Set[string]) bool

insertNewTasks inserts new task refs under the new-tasks divider (creating it if needed). Returns updated save flag.

func insertOverdueTasks

Go
func insertOverdueTasks(page logseq.Page, state *pageState, overdueBlockRefs *set.Set[string])

insertOverdueTasks inserts overdue task refs under the overdue divider (creating it if needed).

Text Only
Sections order: Focus / Overdue / New tasks / all other tasks / Scheduled tasks

Overdue tasks go after the focus section and before new ones so the user can manually decide which overdue tasks deserve focus.

func insertScheduledTasks

Go
func insertScheduledTasks(page logseq.Page, state *pageState, futureScheduledBlockRefs *set.Set[string])

insertScheduledTasks moves future-scheduled tasks to the bottom of the page.

func isDuplicateRef

Go
func isDuplicateRef(uuid string, state *pageState, underTriaged, underScheduled bool) bool

isDuplicateRef returns true if blockRef should be removed as a duplicate. Priority order: Scheduled section wins over all others; Triaged section wins over regular area; first-seen wins in the regular/Unranked area.

func kindName

Go
func kindName(kind directiveKind) string

func nextChildHasPin

Go
func nextChildHasPin(node content.Node) bool

func printQuickCaptureURL

Go
func printQuickCaptureURL(graph *logseq.Graph)

func processAllBlocks

Go
func processAllBlocks(page logseq.Page, state *pageState, obsoleteBlockRefs, overdueBlockRefs, futureScheduledBlockRefs *set.Set[string])

processAllBlocks iterates all blocks on the page, records section dividers, and removes or unpins block refs that are obsolete, overdue, or scheduled.

func processBlockRef

Go
func processBlockRef(node content.Node, blockRef *content.BlockRef, block *content.Block, state *pageState, obsoleteBlockRefs, overdueBlockRefs, futureScheduledBlockRefs *set.Set[string])

processBlockRef decides whether to delete, pin, unpin, or keep a block ref. It also collects directive annotations (CANCELED, WAITING, priority) prepended to the ref.

func queryTasksFromPages

Go
func queryTasksFromPages(graph *logseq.Graph, logseqAPI logseqapi.LogseqAPI, pageTitles []string, currentTime func() time.Time) (*logseqapi.CategorizedTasks, error)

queryTasksFromPages queries Logseq API for tasks from specified pages. It uses concurrent processing for multiple pages and sequential processing for a single page.

func queryTasksFromPagesConcurrent

Go
func queryTasksFromPagesConcurrent(logseqAPI logseqapi.LogseqAPI, pageTitles []string, tasks *logseqapi.CategorizedTasks, finder logseqext.LogseqFinder, currentTime func() time.Time) (*logseqapi.CategorizedTasks, error)

queryTasksFromPagesConcurrent processes pages concurrently using goroutines.

func queryTasksFromPagesSequential

Go
func queryTasksFromPagesSequential(logseqAPI logseqapi.LogseqAPI, pageTitles []string, tasks *logseqapi.CategorizedTasks, finder logseqext.LogseqFinder, currentTime func() time.Time) (*logseqapi.CategorizedTasks, error)

queryTasksFromPagesSequential processes pages sequentially (original implementation).

func queryTasksFromSinglePage

Go
func queryTasksFromSinglePage(logseqAPI logseqapi.LogseqAPI, pageTitle string, finder logseqext.LogseqFinder) ([]logseqapi.TaskJSON, error)

queryTasksFromSinglePage queries tasks from a single page and returns the JSON tasks.

func recordSectionDivider

Go
func recordSectionDivider(block *content.Block, textValue string, state *pageState)

recordSectionDivider updates state with a block if its text matches a known section header.

func reportCounts

Go
func reportCounts(state *pageState, save bool) bool

reportCounts prints colored summaries and returns updated save flag.

func scanPageBlocks

Go
func scanPageBlocks(page logseq.Page, state *pageState, obsoleteBlockRefs, overdueBlockRefs, futureScheduledBlockRefs *set.Set[string])

scanPageBlocks is a two-pass coordinator: first it collects UUIDs already in the Triaged and Scheduled sections, then processes all blocks (which uses those UUIDs for deduplication in the regular area).

func sortTriagedSection

Go
func sortTriagedSection(state *pageState, taskLookup map[logseqapi.TaskUUID]logseqapi.TaskJSON)

sortTriagedSection sorts children of the Triaged divider by priority, date, name, and ID.

func taskSortKeyLess

Go
func taskSortKeyLess(left, right taskSortKey) bool

taskSortKeyLess compares two sort keys for ordering in the Triaged section.

type Backlog

Go
type Backlog interface {
    Graph() *logseq.Graph
    ProcessAll(partialNames []string) error
    ProcessOne(pageTitle string, funcQueryRefs func() (*logseqapi.CategorizedTasks, error)) (*Result, error)
}

func NewBacklog

Go
func NewBacklog(graph *logseq.Graph, logseqAPI logseqapi.LogseqAPI, reader ConfigReader, currentTime func() time.Time) Backlog

type Config

Go
type Config struct {
    FocusPage string
    Backlogs  []SingleBacklogConfig
}

func (*Config) FindBacklogPageTitle

Go
func (c *Config) FindBacklogPageTitle(backlogName string) string

FindBacklogPageTitle looks up the full backlog page path from config by backlog name. Returns empty string if no matching backlog is found.

type ConfigReader

Go
type ConfigReader interface {
    ReadConfig() (*Config, error)
}

func NewPageConfigReader

Go
func NewPageConfigReader(graph *logseq.Graph, configPage string) ConfigReader

NewPageConfigReader creates a new ConfigReader that reads the backlog configuration from a Logseq page.

type Header

Header represents a backlog section divider. Label is the display word(s) without the "tasks" suffix (e.g. "Focus"). String() always returns "Emoji Label tasks".

Go
type Header struct {
    Emoji string
    Label string
}

func (Header) Matches

Go
func (h Header) Matches(blockText string) bool

Matches reports whether blockText contains the label, case-insensitively.

func (Header) NewHeading

Go
func (h Header) NewHeading() *content.Heading

NewHeading returns a level-1 heading node with the canonical header text followed by a [[quick capture]] page link. Use this when creating a new section divider so the user can identify blocks inserted by lqd backlog.

func (Header) String

Go
func (h Header) String() string

String returns the canonical display form: "emoji label tasks".

type Result

Go
type Result struct {
    FocusRefsFromPage *set.Set[string]
    ShowQuickCapture  bool
}

func insertAndRemoveRefs

Go
func insertAndRemoveRefs(graph *logseq.Graph, logseqAPI logseqapi.LogseqAPI, pageTitle string, newBlockRefs, obsoleteBlockRefs, overdueBlockRefs, futureScheduledBlockRefs *set.Set[string], taskLookup map[logseqapi.TaskUUID]logseqapi.TaskJSON, currentTime func() time.Time) (*Result, error)

type SingleBacklogConfig

Go
type SingleBacklogConfig struct {
    BacklogPage string
    Icon        string
    InputPages  []string
}

type backlogImpl

Go
type backlogImpl struct {
    graph        *logseq.Graph
    logseqAPI    logseqapi.LogseqAPI
    configReader ConfigReader
    currentTime  func() time.Time
}

func (*backlogImpl) Graph

Go
func (b *backlogImpl) Graph() *logseq.Graph

func (*backlogImpl) ProcessAll

Go
func (b *backlogImpl) ProcessAll(partialNames []string) error

func (*backlogImpl) ProcessOne

Go
func (b *backlogImpl) ProcessOne(pageTitle string, funcQueryRefs func() (*logseqapi.CategorizedTasks, error)) (*Result, error)

type blockDirective

blockDirective records a pending task modification detected on a backlog page.

Go
type blockDirective struct {
    UUID          string
    Kind          directiveKind
    Priority      content.PriorityValue // only for directivePriority; PriorityNone for other kinds
    DirectiveNode content.Node          // the TaskMarker or Priority node to remove after apply
    BacklogBlock  *content.Block        // the block on the backlog page containing the BlockRef
}

func detectDirectives

Go
func detectDirectives(blockRef *content.BlockRef) []blockDirective

detectDirectives inspects all preceding siblings of the BlockRef in its parent Paragraph. It walks backwards from the BlockRef collecting TaskMarker and Priority nodes until it reaches a non-directive node. Returns all found directives (may be empty).

type directiveGroup

directiveGroup holds all directives targeting the same task UUID.

Go
type directiveGroup struct {
    items     []*blockDirective
    uuid      string
    backlog   *content.Block
    hasCancel bool
}

func groupDirectivesByUUID

Go
func groupDirectivesByUUID(directives []blockDirective) []directiveGroup

groupDirectivesByUUID groups a flat slice of directives into per-UUID groups, preserving insertion order so the task file is opened and saved only once per UUID.

type directiveKind

Go
type directiveKind int

Go
const (
    directiveCancel directiveKind = iota
    directiveWaiting
    directiveTodo
    directivePriority
)

type pageConfigReader

Go
type pageConfigReader struct {
    graph      *logseq.Graph
    configPage string
}

func (*pageConfigReader) ReadConfig

Go
func (p *pageConfigReader) ReadConfig() (*Config, error)

ReadConfig reads the backlog configuration from a Logseq page.

type pageState

pageState holds mutable state accumulated while scanning a backlog page.

Go
type pageState struct {
    firstBlock       *content.Block
    dividerNewTasks  *content.Block
    dividerOverdue   *content.Block
    dividerFocus     *content.Block
    dividerScheduled *content.Block
    dividerTriaged   *content.Block
    dividerUnranked  *content.Block

    deletedCount            int
    movedCount              int
    movedScheduledCount     int
    movedFromScheduledCount int
    unpinnedCount           int

    result             *Result
    pinnedBlockRefs    *set.Set[string]
    triagedBlockRefs   *set.Set[string] // UUIDs already in the Triaged section
    scheduledBlockRefs *set.Set[string] // UUIDs already in the Scheduled section
    seenBlockRefs      *set.Set[string] // UUIDs seen during the current scan (for deduplication)
    unscheduledRefs    *set.Set[string] // UUIDs removed from Scheduled because they lost their scheduled date
    directives         []blockDirective // pending task modifications found on the backlog page
}

func newPageState

Go
func newPageState() *pageState

type taskSortKey

taskSortKey holds the fields used to sort tasks in the Triaged section.

Go
type taskSortKey struct {
    priority    content.PriorityValue // PriorityNone=0 sorts FIRST (unprioritized at top)
    createdDate time.Time             // oldest first
    firstLine   string                // alphabetical tiebreaker
    id          logseqapi.TaskUUID    // UUID, guaranteed unique final tiebreaker
    block       *content.Block        // reference to the block for reordering
}

func newSortKeys

Go
func newSortKeys(children content.BlockList, taskLookup map[logseqapi.TaskUUID]logseqapi.TaskJSON) ([]taskSortKey, int)

newSortKeys creates sort keys for all children of the Triaged section.

dashboard

Go
import "github.com/andreoliwa/logseq-doctor/internal/dashboard"

Index

func MoveToUnranked

Go
func MoveToUnranked(graphPath, backlogPageName string, uuids []string) error

MoveToUnranked moves the given task UUIDs from the regular area of backlogPage to under the "🔢 Unranked tasks" section divider, creating the divider if absent.

graphPath is the path to the Logseq graph root directory. backlogPageName is the page name (e.g. "my-backlog", without .md extension). uuids is the list of task UUIDs to move.

func allSectionHeaders

Go
func allSectionHeaders() []backlog.Header

func collectBlocksToMove

Go
func collectBlocksToMove(page logseq.Page, uuidSet map[string]bool) []*content.Block

collectBlocksToMove returns all blocks (top-level or children of section headers) whose block-ref UUID is in uuidSet. Section header blocks themselves are skipped.

func ensureUnrankedDivider

Go
func ensureUnrankedDivider(page logseq.Page, existing *content.Block) *content.Block

ensureUnrankedDivider returns the existing divider block, or creates and inserts one.

func isSectionHeaderBlock

Go
func isSectionHeaderBlock(block *content.Block) bool

isSectionHeaderBlock reports whether block's text matches any known section header.

groom

Go
import "github.com/andreoliwa/logseq-doctor/internal/groom"

Package groom implements the grooming business logic for Logseq tasks.

Index

Constants

Go
const (
    daysPerYear  = 365
    daysPerMonth = 30
)

Groom action name constants. Use these instead of string literals whenever comparing or switching on GroomAction.Name to avoid typos and enable refactoring.

Go
const (
    GroomActionCancel         = "cancel"
    GroomActionFocus          = "focus"
    GroomActionPriorityHigh   = "priority-high"
    GroomActionPriorityMedium = "priority-medium"
    GroomActionPriorityLow    = "priority-low"
    GroomActionSkip           = "skip"
    GroomActionQuit           = "quit"
)

GroomPropertyGroomed is the Logseq block property key written when a task is groomed.

Go
const GroomPropertyGroomed = "groomed"

Go
const groomSeparator = "--------------------------------------------"

Go
const reGroomDays = 90

Variables

ErrBlockIDMissingInFile is returned when a block UUID exists in the Logseq API/DB but the id:: property is absent from the .md file. This happens for blocks that were assigned a UUID internally by Logseq but have not yet had it written to disk. The fix is to open the block in Logseq (which forces the id:: write-back), then re-run groom.

Go
var ErrBlockIDMissingInFile = errors.New("block id:: not found in file")

errEmptyDuration is returned when an empty duration string is provided.

Go
var errEmptyDuration = errors.New("empty duration string")

groomActions maps single-letter inputs to their corresponding actions.

Go
var groomActions = map[string]*Action{
    "x": {Name: GroomActionCancel, SetsGroomed: true, RequiresFile: true},
    "f": {Name: GroomActionFocus, SetsGroomed: true, RequiresFile: true},
    "a": {Name: GroomActionPriorityHigh, SetsGroomed: true, RequiresFile: true, Priority: content.PriorityHigh},
    "b": {Name: GroomActionPriorityMedium, SetsGroomed: true, RequiresFile: true, Priority: content.PriorityMedium},
    "c": {Name: GroomActionPriorityLow, SetsGroomed: true, RequiresFile: true, Priority: content.PriorityLow},
    "s": {Name: GroomActionSkip, SetsGroomed: false, RequiresFile: false},
    "q": {Name: GroomActionQuit, SetsGroomed: false, RequiresFile: false},
}

func ApplyGroomAction

Go
func ApplyGroomAction(graph *logseq.Graph, groomAPI logseqapi.LogseqAPI, action *Action, task map[string]any, opts *WriteOpts) error

ApplyGroomAction applies a groom action to a Logseq block.

func BuildGroomFilter

Go
func BuildGroomFilter(now time.Time, thresholdDate time.Time) string

BuildGroomFilter builds the PocketBase filter for stale tasks. thresholdDate is the cutoff: tasks with journal before this date are considered stale.

NOTE: Do NOT add scheduled/deadline filters here. PocketBase date fields store null (not empty string) when unset, so `scheduled="` never matches and tasks slip through. Future-date filtering is done in Go via HasFutureDate after fetching. See CLAUDE.md.

func CalculateThresholdDate

Go
func CalculateThresholdDate(base time.Time, olderThan string) (time.Time, error)

CalculateThresholdDate subtracts a human-readable duration from a base time. Uses karrick/tparse for calendar-aware math (proper month/year handling). Accepts formats like: "5 years", "90 days", "6 months", "1 year", "2 weeks".

func EnsureBlockOnDisk

Go
func EnsureBlockOnDisk(graph *logseq.Graph, groomAPI logseqapi.LogseqAPI, task map[string]any) (bool, bool)

EnsureBlockOnDisk checks whether a task's block UUID is present in the Logseq .md file. If the id:: property is missing from disk (Logseq lazy-writes UUIDs), it calls logseq.Editor.upsertBlockProperty via the HTTP API to force Logseq to write it immediately.

Background: Logseq assigns UUIDs to blocks internally but only writes them to .md files when triggered (e.g. a backlink is created, the block is edited, or the Editor API writes it). Without this, groom cannot locate or modify the block by its UUID and would skip the task.

The caller should print the log message to stdout so the user knows what happened.

Returns (exists bool, upserted bool):

  • exists=true means the block is on disk (possibly after the upsert triggered a write).
  • upserted=true means the API call was made (Logseq may need a moment to flush; a second groom run will reliably find it even if the file hasn't been updated within this process).

func FormatGroomSummary

Go
func FormatGroomSummary(counts Counts, remaining int, olderThan string) string

FormatGroomSummary formats the end-of-session summary.

func FormatGroomTask

Go
func FormatGroomTask(task map[string]any, index, total int, now time.Time) string

FormatGroomTask formats a PB task record for terminal display.

func FormatTaskAge

Go
func FormatTaskAge(isoDate string, now time.Time) string

FormatTaskAge returns a human-readable age string like "9 years ago".

func HasRecentDate

Go
func HasRecentDate(task map[string]any, thresholdDate time.Time) bool

HasRecentDate reports whether a task should be excluded from the groom queue because its scheduled or deadline date is newer than thresholdDate (i.e. not yet stale).

A task with scheduled/deadline older than the threshold is stale enough to review. A task with a date newer than the threshold is still active and should be skipped. A task with no date is unaffected by this check.

PocketBase date fields store null (not empty string) when unset, so these comparisons cannot be done reliably in PocketBase query strings. Filter in Go instead. See CLAUDE.md.

func applyActionToBlock

Go
func applyActionToBlock(transaction *logseq.Transaction, action *Action, block *content.Block, groomedDate, uuid string, opts *WriteOpts) error

applyActionToBlock applies the specific groom action to a block.

func applyCancelAction

Go
func applyCancelAction(block *content.Block, cancelTime time.Time) error

applyCancelAction sets the task as canceled. cancelled:: date property is set by SetTaskCanceled; groomed:: is intentionally omitted. Kept as a named function for symmetry with applyFocusAction and applyPriorityAction.

func applyFocusAction

Go
func applyFocusAction(transaction *logseq.Transaction, block *content.Block, groomedDate, uuid, focusPageTitle string) error

applyFocusAction marks the block groomed and adds a reference to the Focus page.

func applyPriorityAction

Go
func applyPriorityAction(transaction *logseq.Transaction, block *content.Block, groomedDate, uuid string, priority content.PriorityValue, opts *WriteOpts) error

applyPriorityAction sets priority on the block, marks it groomed, and adds a reference to the Triaged section of the backlog page (if the task has a backlog).

type Action

Action represents a user's grooming decision.

Go
type Action struct {
    Name         string
    SetsGroomed  bool
    RequiresFile bool
    Priority     content.PriorityValue
}

func ParseAction

Go
func ParseAction(input string, hasBacklog bool) *Action

ParseAction converts a single-letter input to an Action. Returns nil if the input is invalid or requires backlog when none exists.

type Counts

Counts tracks how many tasks received each action.

Go
type Counts struct {
    Cancelled      int
    Focused        int
    PriorityHigh   int
    PriorityMedium int
    PriorityLow    int
    Skipped        int
}

type WriteOpts

WriteOpts holds config-derived values needed for write-back operations. These are passed from cmd/ to avoid circular imports with internal/backlog.

Go
type WriteOpts struct {
    FocusPageTitle       string
    BacklogPageTitle     string
    TriagedSectionText   string
    ScheduledSectionText string
    CurrentTime          func() time.Time
}

logseqext

Go
import "github.com/andreoliwa/logseq-doctor/internal/logseqext"

Package logseqext provides generic extensions to the logseq-go library. Functions here are candidates for upstreaming into logseq-go once that library is restructured.

This package must remain self-contained: it may only import logseq-go, the Go standard library, and golang.org/x packages. It must never import logseq-doctor internal packages (doing so would also create a cyclic import).

Index

Constants

JournalDayDivisorMonth is used to extract the month from a journalDay integer (YYYYMMDD).

Go
const JournalDayDivisorMonth = 100

JournalDayDivisorYear is used to extract the year from a journalDay integer (YYYYMMDD).

Go
const JournalDayDivisorYear = 10000

PropertyCancelled is the Logseq block property key written when a task is cancelled.

Go
const PropertyCancelled = "cancelled"

logseqDateFormat is the Go format string for the current graph's journal title format. Derived from config.edn `:journal/page-title-format` = "EEEE, dd.MM.yyyy" → "Monday, 02.01.2006".

Go
const logseqDateFormat = "Monday, 02.01.2006"

Variables

Regex patterns for extracting tags from task content.

Go
var (
    markdownLinkPattern  = regexp.MustCompile(`\[([^\]]+)\]\([^\)]+\)`)
    pageRefPattern       = regexp.MustCompile(`\[\[([^\]]+)\]\]`)
    bracketHashtagPatter = regexp.MustCompile(`#\[\[([^\]]+)\]\]`)
    simpleHashtagPattern = regexp.MustCompile(`#([\w\-/]+)`)
    timePrefixPattern    = regexp.MustCompile(`^\*?\*?(\d{1,2}:\d{2})\*?\*?\s+`)
)

ErrNoParagraph is returned when SetPriority is called on a block with no paragraph.

Go
var ErrNoParagraph = errors.New("block has no paragraph to insert priority into")

journalTitleFormatRe matches the :journal/page-title-format line in config.edn.

Go
var journalTitleFormatRe = regexp.MustCompile(`(?m):journal/page-title-format\s+"([^"]+)"`)

priorityPrefixRegex matches a priority marker at the start of a line for stripping.

Go
var priorityPrefixRegex = regexp.MustCompile(`^\[#[ABC]\]\s*`)

priorityRegex matches Logseq priority markers like [#A], [#B], [#C] in content strings.

Go
var priorityRegex = regexp.MustCompile(`\[#([ABC])\]`)

func AddSibling

Go
func AddSibling(page logseq.Page, newBlock, before *content.Block, after ...*content.Block)

AddSibling inserts newBlock into page relative to the given anchor blocks. It inserts after the last non-nil block in after, or before the before block, or appends to the page if neither is provided.

func AddTask

Go
func AddTask(opts *AddTaskOptions) error

AddTask adds a task to Logseq. If Key is provided, it searches for an existing task containing that key (case-insensitive) and updates it. Otherwise, creates a new task. If Page is provided, adds to that page. Otherwise, adds to journal for Date. If BlockText is provided, adds as a child of the first block containing that text.

func BlockContentText

Go
func BlockContentText(block *content.Block) string

BlockContentText extracts the text content from a block's content nodes.

func BlockProperties

Go
func BlockProperties(block *content.Block) *content.Properties

BlockProperties returns the Properties node for a task block. logseq-go's block.Properties() only checks the first child — task blocks have a Paragraph first, so it would prepend a new empty Properties node before the TODO line. The parser creates an empty Properties placeholder at position 0; the real properties (id::, collapsed::, etc.) appear after the first Paragraph. This helper finds the first Properties that comes after a Paragraph in the content.

func CleanTaskName

Go
func CleanTaskName(taskContent, marker string) string

CleanTaskName extracts the task name from content: first line, marker stripped, time prefix stripped.

func DateYYYYMMDD

Go
func DateYYYYMMDD(time time.Time) int

DateYYYYMMDD returns the current date in YYYYMMDD format.

func ExtractBlockRefUUID

Go
func ExtractBlockRefUUID(block *content.Block) string

ExtractBlockRefUUID extracts the UUID from a block ref inside a child block.

func ExtractBlockRefUUIDs

Go
func ExtractBlockRefUUIDs(page logseq.Page) []string

ExtractBlockRefUUIDs extracts all block ref UUIDs from a page (ordered).

func ExtractDirectTags

Go
func ExtractDirectTags(contentText string) []string

ExtractDirectTags extracts #hashtags and [[page refs]] from content text.

func ExtractFirstLine

Go
func ExtractFirstLine(taskContent string) string

ExtractFirstLine extracts the first line of task content, stripping the marker and priority.

func FindBlockByIDProperty

Go
func FindBlockByIDProperty(page logseq.Page, uuid string) *content.Block

FindBlockByIDProperty finds a block in a page by its id:: property value. It searches Properties nodes within block content because logseq-go's block.Properties() only checks the first child — task blocks have a paragraph first, then properties.

func FindBlockByKey

Go
func FindBlockByKey(page logseq.Page, parentBlock *content.Block, key string) *content.Block

FindBlockByKey searches for a block containing the specified key (case-insensitive). If parentBlock is provided, searches only among its children. Otherwise, searches in the entire page. Returns the Block if found, nil otherwise.

func FindBlockContainingText

Go
func FindBlockContainingText(page logseq.Page, searchText string) *content.Block

FindBlockContainingText searches for the first block containing the specified text using FindDeep.

func FindTaskMarkerByKey

Go
func FindTaskMarkerByKey(page logseq.Page, parentBlock *content.Block, key string) *content.TaskMarker

FindTaskMarkerByKey searches for a task marker containing the specified key (case-insensitive). If parentBlock is provided, searches only among its children. Otherwise, searches in the entire page. Returns the TaskMarker if found, nil otherwise.

func FormatLogseqDate

Go
func FormatLogseqDate(t time.Time) string

FormatLogseqDate formats a time.Time as a Logseq date string: "[[Saturday, 21.03.2026]]".

func JournalDayToTime

Go
func JournalDayToTime(journalDay int) time.Time

JournalDayToTime converts a Logseq journalDay integer (YYYYMMDD) to a time.Time. Returns zero time for zero input.

func NormalizeTagPrefixes

Go
func NormalizeTagPrefixes(tags []string)

NormalizeTagPrefixes ensures all tags start with "#", are lowercase, and slugified (accents removed, non-alphanumeric chars stripped) to match Python's slugify(t, separator=").

func OpenGraphFromPath

Go
func OpenGraphFromPath(path string) *logseq.Graph

OpenGraphFromPath opens a Logseq graph from the given directory path. Aborts the program if path is empty or the graph cannot be opened, to avoid error-handling boilerplate at every call site.

func OpenPage

Go
func OpenPage(graph *logseq.Graph, pageTitle string) logseq.Page

OpenPage opens a page in the Logseq graph. Aborts the program in case of error to avoid boilerplate at every call site.

func ParseLogseqDate

Go
func ParseLogseqDate(dateStr string) (time.Time, error)

ParseLogseqDate parses a Logseq date string like "[[Saturday, 21.03.2026]]" into a time.Time. Returns zero time (not error) for empty or unparseable strings.

func ParsePriorityFromContent

Go
func ParsePriorityFromContent(contentStr string) content.PriorityValue

ParsePriorityFromContent extracts a priority value from a Logseq API content string. It looks for [#A], [#B], or [#C] patterns and returns the corresponding PriorityValue. Returns PriorityNone if no priority marker is found.

func ReadJournalTitleFormat

Go
func ReadJournalTitleFormat(graphPath string) string

ReadJournalTitleFormat reads the JS-style date format string used for journal page titles from logseq/config.edn (e.g. "EEEE, dd.MM.yyyy"). Returns the Logseq default "EEE do, MMM yyyy" if the file cannot be read or the key is absent. This is a candidate for upstreaming to logseq-go.

func RemoveEmptyBlocks

Go
func RemoveEmptyBlocks(save bool, blocks ...*content.Block) bool

RemoveEmptyBlocks removes blocks that have no child blocks and returns true if any were removed.

func SetPriority

Go
func SetPriority(block *content.Block, priority content.PriorityValue) error

SetPriority sets or replaces the priority marker ([#A]/[#B]/[#C]) on a block. If a Priority node exists, it is updated in place. Otherwise, a new Priority node is inserted after the TaskMarker (or at the start of the first paragraph for plain blocks).

func SetTaskCanceled

Go
func SetTaskCanceled(block *content.Block, date ...time.Time) error

SetTaskCanceled changes the task marker to CANCELED and sets the cancelled:: date property. An optional date argument overrides the default (today). The cancelled:: property is always set.

func SetTaskTodo

Go
func SetTaskTodo(block *content.Block) error

SetTaskTodo changes the task marker to TODO using logseq-go's WithStatus API.

func SetTaskWaiting

Go
func SetTaskWaiting(block *content.Block) error

SetTaskWaiting changes the task marker to WAITING using logseq-go's WithStatus API.

func UniqueStrings

Go
func UniqueStrings(items []string) []string

UniqueStrings deduplicates a string slice preserving order.

func blockHasIDProperty

Go
func blockHasIDProperty(block *content.Block, uuid string) bool

blockHasIDProperty checks whether a block's id:: property matches the given UUID. Searches inside content nodes to handle task blocks where properties follow the paragraph.

func containsTextCaseInsensitive

Go
func containsTextCaseInsensitive(node content.Node, searchTextLower string) bool

containsTextCaseInsensitive checks if a node contains the specified text (case-insensitive). It checks Text, PageLink, Hashtag, CodeSpan, and Link nodes (including link URLs and text content).

func isDescendantBlock

Go
func isDescendantBlock(block, ancestor *content.Block) bool

isDescendantBlock returns true if block is a descendant of ancestor by traversing the parent chain upward.

func replaceCurrentPage

Go
func replaceCurrentPage(query, pageTitle string) string

replaceCurrentPage replaces the current page placeholder in the query with the actual page name.

func slugifyTag

Go
func slugifyTag(s string) string

slugifyTag removes accents and non-alphanumeric characters, then lowercases — matching Python's slugify(tag, separator=") behaviour.

func updateExistingTask

Go
func updateExistingTask(existingTaskMarker *content.TaskMarker, opts *AddTaskOptions) error

func updateTaskBackToTodo

Go
func updateTaskBackToTodo(taskMarker *content.TaskMarker, newName string) error

type AddTaskOptions

AddTaskOptions contains options for adding a task to Logseq.

Go
type AddTaskOptions struct {
    Graph     *logseq.Graph
    Date      time.Time
    Page      string           // Page name to add the task to (empty = journal)
    BlockText string           // Partial text to search for in parent blocks
    Key       string           // Unique key to search for existing task (case-insensitive)
    Name      string           // Short name of the task
    TimeNow   func() time.Time // For testing
}

type LogseqFinder

LogseqFinder provides methods for searching within a Logseq graph.

Go
type LogseqFinder interface {
    FindFirstQuery(pageTitle string) string
}

func NewLogseqFinder

Go
func NewLogseqFinder(graph *logseq.Graph) LogseqFinder

NewLogseqFinder creates a new LogseqFinder backed by the given graph.

type SectionedUUID

SectionedUUID pairs a block-ref UUID with whether it lives under a section header (ranked=false means it is under Overdue, New tasks, Triaged, Scheduled, or Unranked — any divider that is not the implicit "ranked" area at the top).

Go
type SectionedUUID struct {
    UUID   string
    Ranked bool
}

func ExtractSectionedBlockRefUUIDs

Go
func ExtractSectionedBlockRefUUIDs(page logseq.Page, unrankedSectionTexts []string) []SectionedUUID

ExtractSectionedBlockRefUUIDs scans a backlog page and returns every block-ref UUID together with whether it is in the ranked area (above all section dividers) or the unranked area (under any divider whose text matches one of unrankedSectionTexts, case-insensitive substring).

The ranked area is defined as top-level blocks that are NOT section-header blocks and NOT descendants of any section-header block.

type logseqFinderImpl

Go
type logseqFinderImpl struct {
    graph *logseq.Graph
}

func (logseqFinderImpl) FindFirstQuery

Go
func (f logseqFinderImpl) FindFirstQuery(pageTitle string) string

FindFirstQuery returns the first query found on the given page, with the current page placeholder replaced by the actual page name. Returns empty string if no query is found or the page cannot be opened.

pocketbase

Go
import "github.com/andreoliwa/logseq-doctor/internal/pocketbase"

Index

Constants

Go
const (
    pbPollInterval = 200 * time.Millisecond
    pbPollTimeout  = 500 * time.Millisecond
)

DateFormat is the ISO date format used for PocketBase date/datetime record fields.

Go
const DateFormat = "2006-01-02 15:04:05.000Z"

Go
const httpClientTimeout = 30 * time.Second

idMaxLength is UUID (36) + underscore (1) + backlog name (up to 50) = 87.

Go
const idMaxLength = float64(87)

Variables

Sentinel errors for PocketBase client operations.

Go
var (
    ErrCannotConnect    = errors.New("cannot connect to PocketBase")
    ErrAuthFailed       = errors.New("PocketBase authentication failed")
    ErrUnexpectedStatus = errors.New("unexpected status from PocketBase")
)

ErrPocketBaseNotFound is returned when the pocketbase binary cannot be located.

Go
var ErrPocketBaseNotFound = errors.New("pocketbase executable not found in $PATH or ~/.local/bin")

ErrWaitTimeout is returned when WaitForReady times out.

Go
var ErrWaitTimeout = errors.New("timed out waiting for service to be ready")

taskStatusValues are the task status values stored in the PocketBase select field.

Go
var taskStatusValues = []string{
    content.TaskStringTodo, content.TaskStringDoing, content.TaskStringDone,
    content.TaskStringWaiting, content.TaskStringCanceled,
}

func FormatDateLocal

Go
func FormatDateLocal(utcStr string) string

FormatDateLocal parses a PocketBase UTC datetime string and returns it in local time as "YYYY-MM-DD HH:MM". Returns the raw string if parsing fails.

func IsReady

Go
func IsReady(healthURL string) bool

IsReady returns true if healthURL responds with 200 OK within one poll timeout.

func LqdTasksSchema

Go
func LqdTasksSchema() map[string]any

LqdTasksSchema returns the PocketBase collection schema for lqd_tasks. Go code is the source of truth — not PB migrations.

func StartPocketBase

Go
func StartPocketBase(workDir string) (*exec.Cmd, error)

StartPocketBase starts pocketbase serve as a managed subprocess from workDir. Stdout and stderr are inherited so PocketBase logs appear in the same terminal with color. The caller is responsible for calling cmd.Process.Kill() on shutdown.

func WaitForReady

Go
func WaitForReady(healthURL string, timeout time.Duration) error

WaitForReady polls healthURL until it returns 200 OK or timeout elapses.

func findPocketBase

Go
func findPocketBase() (string, error)

findPocketBase resolves the pocketbase executable path. It checks $PATH first, then falls back to \~/.local/bin/pocketbase.

func lqdTasksDataFields

Go
func lqdTasksDataFields() []map[string]any

func lqdTasksFields

Go
func lqdTasksFields() []map[string]any

func lqdTasksIdentityFields

Go
func lqdTasksIdentityFields() []map[string]any

type Client

Client is a minimal PocketBase HTTP client.

Go
type Client struct {
    baseURL    string
    token      string
    httpClient *http.Client
}

func NewClient

Go
func NewClient(baseURL, username, password string) (*Client, error)

NewClient authenticates with PocketBase and returns a ready-to-use client.

func NewClientWithToken

Go
func NewClientWithToken(baseURL, token string) *Client

NewClientWithToken returns a client pre-loaded with an existing auth token. Use when a token has already been obtained (e.g. at dashboard startup) to avoid a redundant authentication round-trip.

func (*Client) CollectionExists

Go
func (c *Client) CollectionExists(name string) (bool, error)

CollectionExists checks if a collection exists in PocketBase.

func (*Client) CreateCollection

Go
func (c *Client) CreateCollection(schema map[string]any) error

CreateCollection creates a new collection with the given schema.

func (*Client) CreateRecord

Go
func (c *Client) CreateRecord(collection string, data map[string]any) error

CreateRecord creates a record in the given collection.

func (*Client) DeleteCollection

Go
func (c *Client) DeleteCollection(name string) error

DeleteCollection deletes a collection by name. It first fetches the collection to get its ID.

func (*Client) DeleteRecord

Go
func (c *Client) DeleteRecord(collection, recordID string) error

DeleteRecord deletes a record by ID from the given collection.

func (*Client) FetchRecords

Go
func (c *Client) FetchRecords(collection, filter, sort string, limit ...int) ([]map[string]any, error)

FetchRecords fetches all records from a collection, handling pagination. Optional filter and sort parameters are passed as PB query params. If limit > 0, fetches at most that many records (single page).

func (*Client) Token

Go
func (c *Client) Token() string

Token returns the current authentication token.

func (*Client) UpdateRecord

Go
func (c *Client) UpdateRecord(collection, recordID string, data map[string]any) error

UpdateRecord updates a record by ID in the given collection.

func (*Client) authenticate

Go
func (c *Client) authenticate(username, password string) error

func (*Client) doRequest

Go
func (c *Client) doRequest(method, path string, body io.Reader) (*http.Response, error)

doRequest sends an authenticated HTTP request to PocketBase.

serve

Go
import "github.com/andreoliwa/logseq-doctor/internal/serve"

Index

func NewProxy

Go
func NewProxy(pbURL, token string) http.Handler

NewProxy returns an http.Handler that reverse-proxies to pbURL, injecting the given Bearer token into every proxied request. Any Authorization header supplied by the browser client is overwritten.

lqdsync

Go
import "github.com/andreoliwa/logseq-doctor/internal/sync"

Index

Constants

Go
const (
    hoursPerDay    = 24
    rankSeedFactor = 1000 // rank is seeded as position × rankSeedFactor on first sync
)

func CalculateRanks

Go
func CalculateRanks(backlogs map[string][]string, backlogOrder []string) map[string][]RankInfo

CalculateRanks assigns ranks for all (uuid, backlog) pairs. A task that appears in multiple backlogs gets one RankInfo per backlog.

func DiffRecords

Go
func DiffRecords(existing, desired []map[string]any) ([]map[string]any, []map[string]any, []string)

DiffRecords compares existing PB records with desired records. Returns slices of records to create, update, and IDs to delete.

func TaskToRecord

Go
func TaskToRecord(task logseqapi.TaskJSON, rank *RankInfo, enrichedTags string, currentTime func() time.Time) map[string]any

TaskToRecord converts a TaskJSON + optional RankInfo to a PocketBase record map.

func determineSortDate

Go
func determineSortDate(scheduledISO, deadlineISO, today string) string

determineSortDate picks the best sort date: scheduled > deadline > today (fallback).

func extractRankFields

Go
func extractRankFields(rank *RankInfo) (string, int, int, int)

extractRankFields returns backlog name, index, section, and rank from a RankInfo pointer.

func indexRecordsByID

Go
func indexRecordsByID(records []map[string]any) map[string]map[string]any

indexRecordsByID builds a map from record "id" field to the full record.

func isDateBeforeToday

Go
func isDateBeforeToday(dateISO string, today time.Time) bool

isDateBeforeToday returns true if the RFC3339 date string is non-empty and before today.

func isOverdue

Go
func isOverdue(scheduledISO, deadlineISO string, currentTime func() time.Time) bool

isOverdue checks if a task is past its scheduled or deadline date. Parses RFC3339 date strings (produced by yyyymmddLocalISO) and compares as dates.

func parseGroomedDate

Go
func parseGroomedDate(task logseqapi.TaskJSON) string

parseGroomedDate extracts the groomed date from task properties.

func recordChanged

Go
func recordChanged(existing, desired map[string]any) bool

recordChanged checks if any sync-relevant fields differ between two records.

func syncUpdateFields

Go
func syncUpdateFields() []string

syncUpdateFields returns the fields checked by recordChanged to detect updates. rank is intentionally excluded — the UI owns rank after record creation.

func yyyymmddToDateOnly

Go
func yyyymmddToDateOnly(dateInt int) string

yyyymmddToDateOnly converts a YYYYMMDD integer to a plain date string (YYYY-MM-DD). Returns empty string for zero values. Used for journal dates where Python sends date.isoformat() without timezone.

func yyyymmddToLocalISO

Go
func yyyymmddToLocalISO(dateInt int) string

yyyymmddToLocalISO converts a YYYYMMDD integer to an RFC3339 datetime string with local timezone offset. Returns empty string for zero values. Used for scheduled/deadline dates where Python uses datetime.strptime().astimezone().isoformat() which includes the local timezone offset, causing PocketBase to shift to UTC on storage.

type RankInfo

RankInfo holds backlog rank data for a task.

Go
type RankInfo struct {
    BacklogName  string
    BacklogIndex int
    Section      int // backlog.SectionRanked, SectionUnranked, or SectionOrphan
    Rank         int
}

testutils

Go
import "github.com/andreoliwa/logseq-doctor/internal/testutils"

Index

Constants

blockIDBase is the starting numeric ID for fixture blocks.

Go
const blockIDBase = 1000

dirPerm is the permission used for directories created by the fixture framework.

Go
const dirPerm = 0o755

filePerm is the permission used for files created by the fixture framework.

Go
const filePerm = 0o600

pageIDBase is the starting numeric ID for fixture pages.

Go
const pageIDBase = 2000

Variables

ExportBuildAPIResponse exposes buildAPIResponse for white-box testing.

Go
var ExportBuildAPIResponse = buildAPIResponse //nolint:gochecknoglobals

ExportBuildSlugMap exposes buildSlugMap for white-box testing.

Go
var ExportBuildSlugMap = buildSlugMap //nolint:gochecknoglobals

ExportCollapseSlugs exposes collapseSlugs for white-box testing.

Go
var ExportCollapseSlugs = collapseSlugs //nolint:gochecknoglobals

ExportExpandSlugs exposes expandSlugs for white-box testing.

Go
var ExportExpandSlugs = expandSlugs //nolint:gochecknoglobals

ExportResolveRelativeDate exposes resolveRelativeDate for white-box testing.

Go
var ExportResolveRelativeDate = resolveRelativeDate //nolint:gochecknoglobals

Go
var baselineTime = time.Date(2025, 4, 13, 3, 33, 0, 0, time.UTC) //nolint:gochecknoglobals

slugRefPattern matches (( any slug with spaces )) in committed .md files.

Go
var slugRefPattern = regexp.MustCompile(`\(\(\s*([^)]+?)\s*\)\)`)

Go
var testStartTime = time.Now() //nolint:gochecknoglobals

func AssertGoldenJournals

Go
func AssertGoldenJournals(t *testing.T, graph *logseq.Graph, caseDirName string, pages []string)

func AssertGoldenPages

Go
func AssertGoldenPages(t *testing.T, graph *logseq.Graph, caseDirName string, pages []string)

func AssertPagesDontExist

Go
func AssertPagesDontExist(t *testing.T, graph *logseq.Graph, pages []string)

func CaptureOutput

Go
func CaptureOutput(function func()) string

CaptureOutput captures both stdout and stderr. It also works with the "color" package.

func ExportFixtureUUID

Go
func ExportFixtureUUID(f *TaskFixture, slug string) string

ExportFixtureUUID returns the UUID for the given slug in the fixture (for white-box testing).

func NewStubGraph

Go
func NewStubGraph(t *testing.T, subDir string) *logseq.Graph

NewStubGraph creates a test graph using the new directory structure. It uses graph-template as the base and loads test data from testdata/{subDir}/journals and testdata/{subDir}/pages.

func RelativeTime

Go
func RelativeTime() time.Time

func assertGoldenContent

Go
func assertGoldenContent(t *testing.T, graph *logseq.Graph, journals bool, caseDirName string, pages []string)

func buildAPIResponse

Go
func buildAPIResponse(blocks []Block, slugToUUIDMap map[string]string, now time.Time) string

buildAPIResponse generates a JSON array string from blocks, matching the Logseq API response format. now is used to resolve relative Scheduled/Deadline/Groomed date strings.

func buildBlockContent

Go
func buildBlockContent(block Block, uuid string) string

buildBlockContent constructs the Logseq block content string.

func buildBlockProperties

Go
func buildBlockProperties(block Block, uuid string, now time.Time) (map[string]string, []string)

buildBlockProperties constructs the properties map and order slice for a block.

func buildBlockRefs

Go
func buildBlockRefs(block Block, tagIDs map[string]int, pageID int) ([]refJSON, []refJSON)

buildBlockRefs builds the refs and pathRefs slices for a block.

func buildSlugMap

Go
func buildSlugMap(blocks []Block) (map[string]string, map[string]string)

buildSlugMap builds slug→UUID and UUID→slug maps from a slice of blocks. Panics if two different slugs produce the same UUID (collision detection).

func buildTagIDs

Go
func buildTagIDs(blocks []Block) map[string]int

buildTagIDs assigns stable numeric IDs to all distinct tags across blocks.

func collapseSlugs

Go
func collapseSlugs(content string, uuidToSlugMap map[string]string) string

collapseSlugs replaces UUIDs with (( slug )) in content. Handles both ((uuid)) block refs and bare UUIDs (e.g. in id:: property lines). UUIDs not present in uuidToSlugMap are left unchanged.

func copyDirTree

Go
func copyDirTree(src, dst string) error

copyDirTree copies a directory tree recursively from src to dst.

func expandSlugs

Go
func expandSlugs(content string, slugToUUIDMap map[string]string) string

expandSlugs replaces (( slug )) with ((uuid)) in content. Slugs not present in slugToUUIDMap are left unchanged.

func resolveJournalDayInt

Go
func resolveJournalDayInt(rel string, now time.Time) int

resolveJournalDayInt resolves a relative date string to a Logseq journalDay int. Returns 0 if rel is empty or invalid.

func resolveRelativeDate

Go
func resolveRelativeDate(rel string, now time.Time) (time.Time, error)

resolveRelativeDate parses a relative date string like "+3d", "-1w", "+2m", "+1y" and returns the resolved time. Empty string returns the zero time. "0" returns now. Supported units: d (days), w (weeks), m (months), y (years).

func slugToUUID

Go
func slugToUUID(slug string) string

slugToUUID derives a deterministic UUID from a slug using FNV-32a. Format: {h8}-0000-0000-0000-{h8}0000 where h8 = zero-padded 8-char hex of fnv32a(slug).

func toJournalDayInt

Go
func toJournalDayInt(t time.Time) int

toJournalDayInt converts a time.Time to the Logseq journalDay integer format (YYYYMMDD).

type Block

Block is a generic Logseq block fixture. Most callers use the Task() constructor which sets Marker. The framework is built on Block internally so non-task blocks (plain content, property blocks) can be added in the future without architectural changes — just add a Block constructor alongside Task().

Go
type Block struct {
    Slug       string
    Marker     string // content.TaskString* from logseq-go (e.g. TaskStringTodo), or "" for non-task blocks
    Text       string
    Tags       []string
    Scheduled  string            // relative date: "+3d", "-1w", "+2m", "+1y", "" for none
    Deadline   string            // same format as Scheduled
    Priority   string            // "A", "B", "C", or ""
    Groomed    string            // relative date for groomed:: property, "" means no property
    JournalDay string            // "2025-03-02" format; defaults to baseline date (2025-04-13)
    ExtraProps map[string]string // arbitrary Logseq block properties
}

func Task

Go
func Task(slug, status, text string, opts ...BlockOpt) Block

Task constructs a Block with Marker set. Use BlockOpts for optional fields.

type BlockOpt

BlockOpt is a functional option for configuring a Block.

Go
type BlockOpt func(*Block)

func WithDeadline

Go
func WithDeadline(rel string) BlockOpt

WithDeadline sets the Deadline relative date.

func WithExtraProps

Go
func WithExtraProps(props map[string]string) BlockOpt

WithExtraProps sets arbitrary Logseq block properties.

func WithGroomed

Go
func WithGroomed(rel string) BlockOpt

WithGroomed sets the Groomed relative date (e.g. "-7d").

func WithJournalDay

Go
func WithJournalDay(day string) BlockOpt

WithJournalDay sets the journal page date ("2025-03-02").

func WithPriority

Go
func WithPriority(p string) BlockOpt

WithPriority sets the Priority ("A", "B", or "C").

func WithScheduled

Go
func WithScheduled(rel string) BlockOpt

WithScheduled sets the Scheduled relative date (e.g. "+3d", "-1w").

func WithTags

Go
func WithTags(tags ...string) BlockOpt

WithTags sets the Tags field.

type TaskFixture

TaskFixture holds block definitions and generates fake API responses for a test.

Go
type TaskFixture struct {
    t          *testing.T
    blocks     []Block
    slugToUUID map[string]string
    uuidToSlug map[string]string
}

func NewFixture

Go
func NewFixture(t *testing.T, blocks ...Block) *TaskFixture

NewFixture creates a TaskFixture from the given blocks.

func (*TaskFixture) Add

Go
func (f *TaskFixture) Add(blocks ...Block) *TaskFixture

Add appends blocks to the fixture and returns it for chaining. Panics if any new slug collides with an existing one.

func (*TaskFixture) AssertGoldenJournals

Go
func (f *TaskFixture) AssertGoldenJournals(t *testing.T, graph *logseq.Graph, caseDirName string, pages []string)

AssertGoldenJournals is like AssertGoldenPages but for journals/.

func (*TaskFixture) AssertGoldenPages

Go
func (f *TaskFixture) AssertGoldenPages(t *testing.T, graph *logseq.Graph, caseDirName string, pages []string)

AssertGoldenPages collapses UUIDs back to slugs in each output page, then compares against golden files in testdata/{caseDirName}/pages/.

func (*TaskFixture) FakeBacklog

Go
func (f *TaskFixture) FakeBacklog(t *testing.T, configPage, caseDirName string) backlog.Backlog

FakeBacklog creates a backlog.Backlog backed by: - a temp graph populated from testdata/{caseDirName}/ with slugs expanded to UUIDs - a fake Logseq API returning generated JSON grouped by tag

configPage is the backlog config page name (e.g. "bk", "ov"). caseDirName is the subdirectory under testdata/ for this test case (may be empty).

func (*TaskFixture) FakeBacklogWithUUIDPages

Go
func (f *TaskFixture) FakeBacklogWithUUIDPages(t *testing.T, configPage, caseDirName string, uuidPageNames map[string]string) backlog.Backlog

FakeBacklogWithUUIDPages creates a backlog.Backlog like FakeBacklog, but also registers UUID-to-page-name mappings in the mock API for FindBlockByUUID calls (used by directives). uuidPageNames maps slug -> page name (e.g. "home-clean-windows" -> "home").

func (*TaskFixture) assertGoldenFiles

Go
func (f *TaskFixture) assertGoldenFiles(t *testing.T, graph *logseq.Graph, journals bool, caseDirName string, pages []string)

func (*TaskFixture) copyExpandedMDs

Go
func (f *TaskFixture) copyExpandedMDs(t *testing.T, src, dst string)

copyExpandedMDs copies .md files from src to dst, expanding slugs to UUIDs in each file. Silently skips if src does not exist.

func (*TaskFixture) fakeAPI

Go
func (f *TaskFixture) fakeAPI(t *testing.T) *mockLogseqAPI

fakeAPI creates a mockLogseqAPI returning generated JSON grouped by tag.

func (*TaskFixture) fakeGraph

Go
func (f *TaskFixture) fakeGraph(t *testing.T, caseDirName string) *logseq.Graph

fakeGraph builds a temp graph with slug-expanded .md files from testdata/{caseDirName}/. Uses the graph-template as the base and expands slugs in the page files.

type blockJSON

blockJSON is the JSON structure matching testdata/stub-api/*.jsonl entries.

Go
type blockJSON struct {
    UUID                 string            `json:"uuid"`
    Marker               string            `json:"marker,omitempty"`
    Content              string            `json:"content"`
    Deadline             int               `json:"deadline,omitempty"`
    Scheduled            int               `json:"scheduled,omitempty"`
    Page                 pageJSON          `json:"page"`
    Properties           map[string]string `json:"properties"`
    PropertiesOrder      []string          `json:"propertiesOrder"`
    PropertiesTextValues map[string]string `json:"propertiesTextValues"`
    Refs                 []refJSON         `json:"refs"`
    PathRefs             []refJSON         `json:"pathRefs"`
    Format               string            `json:"format"`
    ID                   int               `json:"id"`
    Parent               refJSON           `json:"parent"`
    Left                 refJSON           `json:"left"`
}

type mockLogseqAPI

Go
type mockLogseqAPI struct {
    mock.Mock

    tagResponses  map[string]string
    uuidResponses map[string]string // uuid -> page JSON response for FindBlockByUUID
}

func newMockLogseqAPIFromMap

Go
func newMockLogseqAPIFromMap(t *testing.T, responses map[string]string) *mockLogseqAPI

newMockLogseqAPIFromMap creates a mockLogseqAPI that returns pre-built JSON responses keyed by tag.

func (*mockLogseqAPI) PostDatascriptQuery

Go
func (m *mockLogseqAPI) PostDatascriptQuery(query string) (string, error)

func (*mockLogseqAPI) PostQuery

Go
func (m *mockLogseqAPI) PostQuery(query string) (string, error)

func (*mockLogseqAPI) UpsertBlockProperty

Go
func (m *mockLogseqAPI) UpsertBlockProperty(_ string, _ string, _ string) error

func (*mockLogseqAPI) WithUUIDPageResponse

Go
func (m *mockLogseqAPI) WithUUIDPageResponse(uuid, pageJSON string) *mockLogseqAPI

WithUUIDPageResponse registers a page-info JSON response for a given UUID. When FindBlockByUUID queries for this UUID, the mock returns the provided response. The response format matches Logseq's datascript pull result, e.g.:

Text Only
[[{"uuid":"<uuid>","page":{"id":1,"original-name":"PageTitle"}}]]

type pageJSON

Go
type pageJSON struct {
    JournalDay   int    `json:"journalDay"`
    Name         string `json:"name"`
    OriginalName string `json:"originalName"`
    ID           int    `json:"id"`
}

func buildJournalPage

Go
func buildJournalPage(block Block, now time.Time, pageID int) pageJSON

buildJournalPage derives the page JSON for a block.

type refJSON

Go
type refJSON struct {
    ID int `json:"id"`
}

set

Go
import "github.com/andreoliwa/logseq-doctor/pkg/set"

Index

type Set

Set is a simple implementation of a set using a map.

Go
type Set[T cmp.Ordered] struct {
    data map[T]struct{}
}

func NewSet

Go
func NewSet[T cmp.Ordered]() *Set[T]

NewSet creates and returns a new set.

func (*Set[T]) Add

Go
func (s *Set[T]) Add(value T)

Add inserts an element into the set.

func (*Set[T]) Clear

Go
func (s *Set[T]) Clear()

Clear removes all elements from the set.

func (*Set[T]) Contains

Go
func (s *Set[T]) Contains(value T) bool

Contains checks if an element exists in the set.

func (*Set[T]) Diff

Go
func (s *Set[T]) Diff(sets ...*Set[T]) *Set[T]

Diff returns a new set containing elements that are in the current set but not in the provided sets.

func (*Set[T]) Remove

Go
func (s *Set[T]) Remove(value T)

Remove deletes an element from the set.

func (*Set[T]) Size

Go
func (s *Set[T]) Size() int

Size returns the number of elements in the set.

func (*Set[T]) Update

Go
func (s *Set[T]) Update(sets ...*Set[T])

Update adds all elements from the given sets into the current set.

func (*Set[T]) Values

Go
func (s *Set[T]) Values() []T

Values returns all elements in the set as a slice.

func (*Set[T]) ValuesSorted

Go
func (s *Set[T]) ValuesSorted() []T

ValuesSorted returns all elements in the set as a sorted slice.

Generated by gomarkdoc