Skip to content

Instantly share code, notes, and snippets.

@mallomar
Last active September 26, 2025 18:20
Show Gist options
  • Select an option

  • Save mallomar/32314dac4fefd5e5f8838dc3b0646480 to your computer and use it in GitHub Desktop.

Select an option

Save mallomar/32314dac4fefd5e5f8838dc3b0646480 to your computer and use it in GitHub Desktop.
Modifies the Anki plugin to export book title, chapter title and book reference page to the sentence context field (requires Anki plugin: https://github.com/Ajatt-Tools/anki.koplugin)
-- anki-modify.lua - Version that uses proper EPUB navigation for page mapping
local logger = require("logger")
logger.info("=== ANKI CONTEXT PATCH LOADING ===")
local function patch_anki_note()
local success, AnkiNote = pcall(require, "ankinote")
if not success then
logger.warn("Anki Context Patch: Could not load ankinote module")
return false
end
logger.info("Anki Context Patch: Patching AnkiNote:get_word_context")
-- Store the original method
local original_get_word_context = AnkiNote.get_word_context
-- Create our enhanced version
function AnkiNote:get_word_context(word)
logger.info("AnkiNote#get_custom_context() " .. tostring(self.prev_context_size) ..
" " .. tostring(self.next_context_size) ..
" " .. tostring(self.prev_context_num) ..
" " .. tostring(self.next_context_num))
local context_text = ""
local UIManager = require("ui/uimanager")
local reader_ui = nil
local document = nil
local view = nil
local book_title = ""
local chapter_title = ""
-- Method 1: Check UIManager for running instance
if UIManager._running_instance then
reader_ui = UIManager._running_instance
logger.info("Anki Context Patch: Found UIManager._running_instance")
end
-- Method 2: Look through active widgets
if not reader_ui and UIManager.active_widgets then
logger.info("Anki Context Patch: Checking active_widgets, count: " .. tostring(#UIManager.active_widgets))
for i, widget in ipairs(UIManager.active_widgets) do
logger.info("Anki Context Patch: Widget " .. i .. " type: " .. tostring(type(widget)))
if widget and widget.document and widget.view then
reader_ui = widget
logger.info("Anki Context Patch: Found reader in active_widgets")
break
elseif widget and widget.ui and widget.ui.document then
reader_ui = widget.ui
logger.info("Anki Context Patch: Found reader via widget.ui")
break
end
end
end
-- Method 3: Try global access patterns
if not reader_ui then
-- Look for common global variables that might contain the reader
local possible_globals = {"_G", "require('apps/reader/readerui')", "package.loaded['apps/reader/readerui']"}
for _, global_name in ipairs(possible_globals) do
local success, result = pcall(function()
if global_name == "_G" then
-- Check if there's a reader instance in the global table
for k, v in pairs(_G) do
if type(v) == "table" and v.document and v.view then
return v
end
end
else
local module = loadstring("return " .. global_name)()
if module and module.instance then
return module.instance
end
end
return nil
end)
if success and result then
reader_ui = result
logger.info("Anki Context Patch: Found reader via " .. global_name)
break
end
end
end
if reader_ui then
document = reader_ui.document
view = reader_ui.view
logger.info("Anki Context Patch: Successfully found document and view")
-- Get book title using document metadata and better title extraction
if document then
-- Try to get title from document properties first
if document.props and document.props.title then
book_title = document.props.title
logger.info("Anki Context Patch: Book title from document props: " .. book_title)
elseif reader_ui.doc_props and reader_ui.doc_props.title then
book_title = reader_ui.doc_props.title
logger.info("Anki Context Patch: Book title from reader doc_props: " .. book_title)
elseif document.file then
-- Better file path parsing - extract from directory structure
local filename = document.file
logger.info("Anki Context Patch: Full file path: " .. filename)
-- Try to extract from the directory name which should contain the full title
-- Pattern: .../Author/Book Title (###)/filename.epub
local dir_title = filename:match("/([^/]+)%s*%([^)]+%)/[^/]+%.%w+$")
if dir_title then
book_title = dir_title:gsub("_", " "):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
logger.info("Anki Context Patch: Book title from directory: " .. book_title)
else
-- Fallback: extract from filename but handle it better
local title_part = filename:match("([^/]+)%.%w+$") or "Unknown Book"
-- Remove author and catalog info
local clean_title = title_part:match("^(.+)%s*%-%s*[^-]+$") or title_part
clean_title = clean_title:gsub("_", " "):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
book_title = clean_title
logger.info("Anki Context Patch: Book title from filename: " .. book_title)
end
end
end
-- Keep the existing chapter detection exactly as it is (WORKING)
local current_page = nil
local current_pos = nil
if view then
-- Try different methods to get current page
if view.getCurrentPage then
current_page = view:getCurrentPage()
elseif view.state and view.state.page then
current_page = view.state.page
elseif view.current_page then
current_page = view.current_page
end
logger.info("Anki Context Patch: Current page: " .. tostring(current_page))
end
-- Also try to get the current document position
if document then
if document.getXPointer then
current_pos = document:getXPointer()
logger.info("Anki Context Patch: Current position: " .. tostring(current_pos))
end
end
-- Try to find chapter using table of contents and current position
if reader_ui.toc and reader_ui.toc.toc then
logger.info("Anki Context Patch: TOC available with " .. tostring(#reader_ui.toc.toc) .. " entries")
-- Method 1: Try using current position if available
if current_pos and reader_ui.toc.getChapterByXPointer then
local chapter = reader_ui.toc:getChapterByXPointer(current_pos)
if chapter and chapter.title then
chapter_title = chapter.title
logger.info("Anki Context Patch: Chapter title via XPointer: " .. chapter_title)
end
end
-- Method 2: Try using page number with better matching
if chapter_title == "" and current_page then
local best_chapter = nil
local best_distance = math.huge
for _, chapter in ipairs(reader_ui.toc.toc) do
if chapter.page and chapter.title then
-- Find the chapter that's closest but not after current page
if chapter.page <= current_page then
local distance = current_page - chapter.page
if distance < best_distance then
best_distance = distance
best_chapter = chapter
end
end
end
end
if best_chapter then
chapter_title = best_chapter.title
logger.info("Anki Context Patch: Chapter title via best match: " .. chapter_title .. " (page " .. best_chapter.page .. ")")
end
end
-- Method 3: Try direct chapter lookup if available
if chapter_title == "" and reader_ui.toc.getCurrentChapter then
local current_chapter = reader_ui.toc:getCurrentChapter()
if current_chapter and current_chapter.title then
chapter_title = current_chapter.title
logger.info("Anki Context Patch: Chapter title via getCurrentChapter: " .. chapter_title)
end
end
end
else
logger.warn("Anki Context Patch: Could not find reader_ui instance")
end
-- NEW: Proper EPUB page mapping using navigation document
local reference_page = nil
if document and view then
logger.info("Anki Context Patch: Attempting to find reference page using EPUB navigation")
-- Method 1: Try to access the navigation document directly
if document.getNavigation then
local nav_doc = document:getNavigation()
if nav_doc and nav_doc.page_list then
logger.info("Anki Context Patch: Found navigation page list")
-- Current document position/page
local current_display_page = view.state and view.state.page or (view.getCurrentPage and view:getCurrentPage())
local current_xpointer = document.getXPointer and document:getXPointer()
-- Try to map current position to reference page using nav page list
for _, page_entry in ipairs(nav_doc.page_list) do
if page_entry.href and page_entry.number then
-- Check if current position matches this page entry
if current_xpointer and page_entry.href:find(current_xpointer, 1, true) then
reference_page = tonumber(page_entry.number)
logger.info("Anki Context Patch: Found reference page via nav XPointer match: " .. tostring(reference_page))
break
end
end
end
end
end
-- Method 2: Try to access EPUB page mapping through document structure
if not reference_page and document.getPageMap then
local page_map = document:getPageMap()
if page_map then
logger.info("Anki Context Patch: Found document page map with " .. tostring(#page_map) .. " entries")
local current_display_page = view.state and view.state.page or (view.getCurrentPage and view:getCurrentPage())
local current_pos = document.getXPointer and document:getXPointer()
-- Try different approaches to map current position
if current_pos then
logger.info("Anki Context Patch: Looking for position " .. current_pos .. " in page map")
for i, page_info in ipairs(page_map) do
if type(page_info) == "table" then
-- Method A: Check if CFI/href matches current position
if page_info.cfi and current_pos:find(page_info.cfi, 1, true) then
reference_page = page_info.label or page_info.number
if reference_page then
logger.info("Anki Context Patch: Found reference page via CFI match: " .. tostring(reference_page))
break
end
end
-- Method B: Check href patterns
if page_info.href then
local href_file = page_info.href:match("([^#]+)")
local current_file = current_pos:match("([^/]+)")
if href_file and current_file and current_pos:find(href_file, 1, true) then
reference_page = page_info.label or page_info.number
if reference_page then
logger.info("Anki Context Patch: Found reference page via href match: " .. tostring(reference_page))
break
end
end
end
-- Method C: Try page number correlation
if current_display_page and page_info.page then
local map_page = tonumber(page_info.page)
if map_page and map_page == current_display_page then
-- Keep page label as string to handle roman numerals (i, ii, iii) and other formats
reference_page = page_info.label or page_info.number
if reference_page then
logger.info("Anki Context Patch: Found reference page via page number match: " .. tostring(reference_page))
break
end
end
end
elseif type(page_info) == "string" then
-- Handle string-based page info
local page_num = page_info:match("(%d+)")
if page_num then
reference_page = page_num -- Keep as string
logger.info("Anki Context Patch: Found reference page from string entry: " .. tostring(reference_page))
break
end
end
end
end
end
end
-- Method 3: Try to get page number from the document's internal page system
if not reference_page and document.getPageFromXPointer then
local current_pos = document:getXPointer()
if current_pos then
-- Try with different parameters to get reference page
local possible_pages = {
document:getPageFromXPointer(current_pos, true), -- Reference pages
document:getPageFromXPointer(current_pos, "reference"),
document:getPageFromXPointer(current_pos, "original")
}
for _, page_num in ipairs(possible_pages) do
if page_num and type(page_num) == "number" and page_num > 0 and page_num < 1000 then
local current_display = view.state and view.state.page or 29 -- fallback
if page_num ~= current_display then -- Different from display page
reference_page = page_num
logger.info("Anki Context Patch: Found reference page via getPageFromXPointer: " .. tostring(reference_page))
break
end
end
end
end
end
-- Method 4: Try to parse navigation from document directly
if not reference_page and document.readFile then
local nav_success, nav_content = pcall(function()
return document:readFile("nav.xhtml") or document:readFile("toc.ncx") or document:readFile("navigation.xhtml")
end)
if nav_success and nav_content then
logger.info("Anki Context Patch: Found navigation file content")
local current_pos = document:getXPointer()
if current_pos then
-- Parse the navigation content to find page mappings
-- Look for page-list entries that match current position
for page_ref in nav_content:gmatch('<a href="([^"]+)">(%d+)</a>') do
local href, page_num = page_ref:match("([^>]+)>(%d+)")
if href and current_pos:find(href, 1, true) then
reference_page = tonumber(page_num)
if reference_page then
logger.info("Anki Context Patch: Found reference page via nav parsing: " .. tostring(reference_page))
break
end
end
end
end
end
end
-- Fallback: If all else fails, use display page but log it
if not reference_page then
local display_page = view.state and view.state.page or (view.getCurrentPage and view:getCurrentPage())
if display_page then
reference_page = display_page
logger.warn("Anki Context Patch: Using display page as final fallback: " .. tostring(reference_page))
end
end
end
-- Build page and source info
local source_info = ""
if book_title ~= "" then
source_info = book_title
if chapter_title ~= "" then
source_info = source_info .. ", " .. chapter_title
end
if reference_page then
source_info = source_info .. ", Page " .. tostring(reference_page)
end
elseif reference_page then
source_info = "Page " .. tostring(reference_page)
end
-- Get the original context
local original_result = original_get_word_context(self, word)
if original_result and source_info ~= "" then
-- If we have both context and source info, combine them
context_text = original_result .. " - " .. source_info
elseif original_result then
-- If we only have original context
context_text = original_result
elseif source_info ~= "" then
-- If we only have source info
context_text = source_info
else
-- Fallback
context_text = "Context not available"
end
logger.info("Anki Context Patch: Final context: " .. context_text)
return context_text
end
logger.info("Anki Context Patch: Successfully patched AnkiNote:get_word_context")
return true
end
-- Try to patch immediately
if not patch_anki_note() then
logger.warn("=== ANKI CONTEXT PATCH FAILED ===")
logger.info("Anki Context Patch: Initial patch failed, scheduling delayed attempts")
local UIManager = require("ui/uimanager")
-- Schedule multiple retry attempts
local retry_count = 0
local max_retries = 5
local function retry_patch()
retry_count = retry_count + 1
logger.info("Anki Context Patch: Retry attempt " .. retry_count .. "/" .. max_retries)
if patch_anki_note() then
logger.info("=== ANKI CONTEXT PATCH APPLIED SUCCESSFULLY ===")
return
end
if retry_count < max_retries then
UIManager:scheduleIn(2, retry_patch)
else
logger.warn("=== ANKI CONTEXT PATCH FAILED AFTER ALL RETRIES ===")
end
end
UIManager:scheduleIn(1, retry_patch)
else
logger.info("=== ANKI CONTEXT PATCH APPLIED SUCCESSFULLY ===")
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment