Merge branch 'dev' into index-confilicts-readable-typst

This commit is contained in:
Linus
2025-01-25 18:46:56 +01:00
committed by GitHub
14 changed files with 348 additions and 70 deletions

View File

@@ -14,7 +14,7 @@ Available snippets can mostly be intuitively derived from [here](././lua/typstar
Markup snippets: Markup snippets:
- Begin inline math with `ll` and multiline math with `dm` - Begin inline math with `ll` and multiline math with `dm`
- [Markup shorthands](./lua/typstar/snippets/markup.lua) (e.g. `HIG` &#8594; `#highlight[<cursor>]`, `IMP` &#8594; `$=>$ `) - [Markup shorthands](./lua/typstar/snippets/markup.lua) (e.g. `HIG` &#8594; `#highlight[<cursor>]`, `IMP` &#8594; `$==>$ `)
- [ctheorems shorthands](./lua/typstar/snippets/markup.lua) (e.g. `tem` &#8594; empty theorem, `exa` &#8594; empty example) - [ctheorems shorthands](./lua/typstar/snippets/markup.lua) (e.g. `tem` &#8594; empty theorem, `exa` &#8594; empty example)
- [Flashcards](#anki): `fla` and `flA` - [Flashcards](#anki): `fla` and `flA`
- All above snippets support visual mode via the [selection key](#installation) - All above snippets support visual mode via the [selection key](#installation)
@@ -24,6 +24,7 @@ Math snippets:
- Alphanumeric characters: `:<char>` &#8594; `$<char>$ ` in markup (e.g. `:X` &#8594; `$X$ `, `:5` &#8594; `$5$ `) - Alphanumeric characters: `:<char>` &#8594; `$<char>$ ` in markup (e.g. `:X` &#8594; `$X$ `, `:5` &#8594; `$5$ `)
- Greek letters: `;<latin>` &#8594; `<greek>` in math and `$<greek>$ ` in markup (e.g. `;a` &#8594; `alpha`/`$alpha$ `) - Greek letters: `;<latin>` &#8594; `<greek>` in math and `$<greek>$ ` in markup (e.g. `;a` &#8594; `alpha`/`$alpha$ `)
- Common indices (numbers and letters `i-n`): `<letter><index> ` &#8594; `<letter>_<index> ` in math and `$<letter>$<index> ` &#8594; `$<letter>_<index>$ ` in markup (e.g `A314 ` &#8594; `A_314 `, `$alpha$n ` &#8594; `$alpha_n$ `) - Common indices (numbers and letters `i-n`): `<letter><index> ` &#8594; `<letter>_<index> ` in math and `$<letter>$<index> ` &#8594; `$<letter>_<index>$ ` in markup (e.g `A314 ` &#8594; `A_314 `, `$alpha$n ` &#8594; `$alpha_n$ `)
- Series of numbered letters: `<letter> ot<optional last index> ` &#8594; `<letter>_1, <letter>_2, ... ` (e.g. `a ot ` &#8594; `a_1, a_2, ... `, `a ot4 ` &#8594; `a_1, a_2, a_3, a_4 `, `alpha ot k ` &#8594; `alpha_1, alpha_2, ..., alpha_k `)
- Wrapping of any mathematical expression (see [operations](./lua/typstar/snippets/visual.lua), works nested, multiline and in visual mode via the [selection key](#installation)): `<expression><operation>` &#8594; `<operation>(<expression>)` (e.g. `(a^2+b^2)rt` &#8594; `sqrt(a^2+b^2)`, `lambdatd` &#8594; `tilde(lambda)`, `(1+1)sQ` &#8594; `[1+1]`, `(1+1)sq` &#8594; `[(1+1)]`) - Wrapping of any mathematical expression (see [operations](./lua/typstar/snippets/visual.lua), works nested, multiline and in visual mode via the [selection key](#installation)): `<expression><operation>` &#8594; `<operation>(<expression>)` (e.g. `(a^2+b^2)rt` &#8594; `sqrt(a^2+b^2)`, `lambdatd` &#8594; `tilde(lambda)`, `(1+1)sQ` &#8594; `[1+1]`, `(1+1)sq` &#8594; `[(1+1)]`)
- Matrices: `<size>ma` and `<size>lma` (e.g. `23ma` &#8594; 2x3 matrix) - Matrices: `<size>ma` and `<size>lma` (e.g. `23ma` &#8594; 2x3 matrix)
@@ -65,19 +66,51 @@ To render the flashcard in your document as well add some code like this
- Use `:TypstarAnkiScan` to scan the current nvim working directory and compile all flashcards in its context, unchanged files will be ignored - Use `:TypstarAnkiScan` to scan the current nvim working directory and compile all flashcards in its context, unchanged files will be ignored
- Use `:TypstarAnkiForce` to force compilation of all flashcards in the current working directory even if the files haven't changed since the last scan (e.g. on preamble change) - Use `:TypstarAnkiForce` to force compilation of all flashcards in the current working directory even if the files haven't changed since the last scan (e.g. on preamble change)
- Use `:TypstarAnkiForceCurrent` to force compilation of all flashcards in the file currently edited - Use `:TypstarAnkiForceCurrent` to force compilation of all flashcards in the file currently edited
- Use `:TypstarAnkiReimport` to also add flashcards that have already been asigned an id but are not currently
present in Anki
- Use `:TypstarAnkiForceReimport` and `:TypstarAnkiForceCurrentReimport` to combine features accordingly
#### Standalone #### Standalone
- Run `typstar-anki --help` to show the available options - Run `typstar-anki --help` to show the available options
## Installation ## Installation
Install the plugin in Neovim and set the `typstarRoot` config or alternatively clone typstar into `~/typstar`. Install the plugin in Neovim and run the plugin setup.
```lua ```lua
require('typstar').setup({ require('typstar').setup({ -- depending on your neovim plugin system
typstarRoot = '/path/to/typstar/repo' -- depending on your nvim plugin system -- your typstar config goes here
}) })
``` ```
### In a Nix Flake
You can add typstart to your `nix-flake` like so:
```nix
# `flake.nix`
inputs = {
# ... other inputs
typstar = {
url = "github:arne314/typstar";
flake = false;
};
}
```
Now you can use `typstar` in any package-set
```nix
with pkgs [
#
# other packges ...
#
(pkgs.vimUtils.buildVimPlugin {
name = "typstar";
src = inputs.typstar;
buildInputs = [
vimPlugins.luasnip
vimPlugins.nvim-treesitter-parsers.typst
];
})
]
```
### Snippets ### Snippets
1. Install [LuaSnip](https://github.com/L3MON4D3/LuaSnip/), set `enable_autosnippets = true` and set a visual mode selection key (e.g. `store_selection_keys = '<Tab>'`) in the configuration 1. Install [LuaSnip](https://github.com/L3MON4D3/LuaSnip/), set `enable_autosnippets = true` and set a visual mode selection key (e.g. `store_selection_keys = '<Tab>'`) in the configuration
2. Install [jsregexp](https://github.com/kmarius/jsregexp) as described [here](https://github.com/L3MON4D3/LuaSnip/blob/master/DOC.md#transformations) (You will see a warning on startup if jsregexp isn't installed properly) 2. Install [jsregexp](https://github.com/kmarius/jsregexp) as described [here](https://github.com/L3MON4D3/LuaSnip/blob/master/DOC.md#transformations) (You will see a warning on startup if jsregexp isn't installed properly)
@@ -88,6 +121,7 @@ require('typstar').setup({
1. Install [Obsidian](https://obsidian.md/) and create a vault in your typst note taking directory 1. Install [Obsidian](https://obsidian.md/) and create a vault in your typst note taking directory
2. Install the [obsidian-excalidraw-plugin](https://github.com/zsviczian/obsidian-excalidraw-plugin) and enable `Auto-export SVG` (in plugin settings at `Embedding Excalidraw into your Notes and Exporting > Export Settings > Auto-export Settings`) 2. Install the [obsidian-excalidraw-plugin](https://github.com/zsviczian/obsidian-excalidraw-plugin) and enable `Auto-export SVG` (in plugin settings at `Embedding Excalidraw into your Notes and Exporting > Export Settings > Auto-export Settings`)
3. Have the `xdg-open` command working or set a different command at `uriOpenCommand` in the [config](#configuration) 3. Have the `xdg-open` command working or set a different command at `uriOpenCommand` in the [config](#configuration)
4. If you encounter issues try cloning the repo into `~/typstar` or setting the `typstarRoot` config accordingly, feel free to open an issue
### Anki ### Anki
0. Typst version `0.12.0` or higher is required 0. Typst version `0.12.0` or higher is required

58
flake.lock generated Normal file
View File

@@ -0,0 +1,58 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1736143030,
"narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1737525964,
"narHash": "sha256-3wFonKmNRWKq1himW9N3TllbeGIHFACI5vmLpk6moF8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5757bbb8bd7c0630a0cc4bb19c47e588db30b97c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1735774519,
"narHash": "sha256-CewEm1o2eVAnoqb6Ml+Qi9Gg/EfNAxbRx1lANGVyoLI=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/e9b51731911566bbf7e4895475a87fe06961de0b.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/e9b51731911566bbf7e4895475a87fe06961de0b.tar.gz"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

64
flake.nix Normal file
View File

@@ -0,0 +1,64 @@
{
description = "typstar nix flake for development";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
};
outputs = inputs @ {
self,
nixpkgs,
flake-parts,
...
}:
flake-parts.lib.mkFlake {inherit inputs;} {
systems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
perSystem = {system, ...}: let
pkgs = import nixpkgs {inherit system;};
typstar = pkgs.vimUtils.buildVimPlugin {
name = "typstar";
src = self;
buildInputs = [
pkgs.vimPlugins.luasnip
pkgs.vimPlugins.nvim-treesitter-parsers.typst
];
};
in {
packages = {
default = typstar;
nvim = let
config = pkgs.neovimUtils.makeNeovimConfig {
customRC = ''
lua << EOF
print("Welcome to Typstar! This is just a demo.")
require('nvim-treesitter.configs').setup {
highlight = { enable = true },
}
require('luasnip').config.set_config({
enable_autosnippets = true,
})
require('typstar').setup()
EOF
'';
plugins = [
typstar
pkgs.vimPlugins.luasnip
pkgs.vimPlugins.nvim-treesitter
pkgs.vimPlugins.nvim-treesitter-parsers.typst
];
};
in
pkgs.wrapNeovimUnstable pkgs.neovim-unwrapped config;
};
};
};
}

View File

@@ -22,8 +22,14 @@ end
function M.scan() run_typstar_anki('') end function M.scan() run_typstar_anki('') end
function M.scan_reimport() run_typstar_anki('--reimport') end
function M.scan_force() run_typstar_anki('--force-scan ' .. vim.fn.getcwd()) end function M.scan_force() run_typstar_anki('--force-scan ' .. vim.fn.getcwd()) end
function M.scan_force_reimport() run_typstar_anki('--reimport --force-scan ' .. vim.fn.getcwd()) end
function M.scan_force_current() run_typstar_anki('--force-scan ' .. vim.fn.expand('%:p')) end function M.scan_force_current() run_typstar_anki('--force-scan ' .. vim.fn.expand('%:p')) end
function M.scan_force_current_reimport() run_typstar_anki('--reimport --force-scan ' .. vim.fn.expand('%:p')) end
return M return M

View File

@@ -46,12 +46,13 @@ end
function M.snip(trigger, expand, insert, condition, priority, wordTrig) function M.snip(trigger, expand, insert, condition, priority, wordTrig)
priority = priority or 1000 priority = priority or 1000
if wordTrig == nil then wordTrig = true end
return luasnip.snippet( return luasnip.snippet(
{ {
trig = trigger, trig = trigger,
trigEngine = M.engine, trigEngine = M.engine,
trigEngineOpts = { condition = condition }, trigEngineOpts = { condition = condition, wordTrig = wordTrig },
wordTrig = wordTrig, wordTrig = false,
priority = priority, priority = priority,
snippetType = 'autosnippet', snippetType = 'autosnippet',
}, },
@@ -68,6 +69,17 @@ end
function M.engine(trigger, opts) function M.engine(trigger, opts)
local base_engine = lsengines.ecma(trigger, opts) local base_engine = lsengines.ecma(trigger, opts)
-- determine possibly fixed length of trigger
local fixed_length
if not trigger:match('[%+%*%?%]%[|]') then
fixed_length = #trigger
- utils.count_string(trigger, '\\')
- utils.count_string(trigger, '%(')
- utils.count_string(trigger, '%)')
end
-- cache preanalysis results
local condition = function() local condition = function()
local cached = lexical_result_cache[opts.condition] local cached = lexical_result_cache[opts.condition]
if cached ~= nil and cached[1] == last_keystroke_time then return cached[2] end if cached ~= nil and cached[1] == last_keystroke_time then return cached[2] end
@@ -75,9 +87,28 @@ function M.engine(trigger, opts)
lexical_result_cache[opts.condition] = { last_keystroke_time, result } lexical_result_cache[opts.condition] = { last_keystroke_time, result }
return result return result
end end
-- matching
return function(line, trig) return function(line, trig)
if not M.snippets_toggle or not condition() then return nil end if not M.snippets_toggle or not condition() then return nil end
return base_engine(line, trig) if fixed_length ~= nil then
local first_idx = #line - fixed_length -- include additional char for wordtrig
if first_idx < 0 then
return nil
elseif first_idx > 0 then
if string.byte(line, first_idx) > 127 then return nil end
end
line = line:sub(first_idx)
end
local whole, captures = base_engine(line, trig)
if whole == nil then return nil end
-- custom word trig
local from = #line - #whole + 1
if opts.wordTrig and from ~= 1 and string.match(string.sub(line, from - 1, from - 1), '[%w.]') ~= nil then
return nil
end
return whole, captures
end end
end end

View File

@@ -1,7 +1,7 @@
local M = {} local M = {}
local default_config = { local default_config = {
typstarRoot = '~/typstar', typstarRoot = nil,
anki = { anki = {
typstarAnkiCmd = 'typstar-anki', typstarAnkiCmd = 'typstar-anki',
typstCmd = 'typst', typstCmd = 'typst',
@@ -30,6 +30,9 @@ local default_config = {
function M.merge_config(args) function M.merge_config(args)
M.config = vim.tbl_deep_extend('force', default_config, args or {}) M.config = vim.tbl_deep_extend('force', default_config, args or {})
M.config.typstarRoot = M.config.typstarRoot
or debug.getinfo(1).source:match('^@(.*)/lua/typstar/config%.lua$')
or '~/typstar'
M.config.excalidraw.templatePath = M.config.excalidraw.templatePath M.config.excalidraw.templatePath = M.config.excalidraw.templatePath
or { or {
['%.excalidraw%.md$'] = M.config.typstarRoot .. '/res/excalidraw_template.excalidraw.md', ['%.excalidraw%.md$'] = M.config.typstarRoot .. '/res/excalidraw_template.excalidraw.md',

View File

@@ -14,8 +14,11 @@ M.setup = function(args)
vim.api.nvim_create_user_command('TypstarOpenExcalidraw', excalidraw.open_drawing, {}) vim.api.nvim_create_user_command('TypstarOpenExcalidraw', excalidraw.open_drawing, {})
vim.api.nvim_create_user_command('TypstarAnkiScan', anki.scan, {}) vim.api.nvim_create_user_command('TypstarAnkiScan', anki.scan, {})
vim.api.nvim_create_user_command('TypstarAnkiReimport', anki.scan_reimport, {})
vim.api.nvim_create_user_command('TypstarAnkiForce', anki.scan_force, {}) vim.api.nvim_create_user_command('TypstarAnkiForce', anki.scan_force, {})
vim.api.nvim_create_user_command('TypstarAnkiForceReimport', anki.scan_force_reimport, {})
vim.api.nvim_create_user_command('TypstarAnkiForceCurrent', anki.scan_force_current, {}) vim.api.nvim_create_user_command('TypstarAnkiForceCurrent', anki.scan_force_current, {})
vim.api.nvim_create_user_command('TypstarAnkiForceCurrentReimport', anki.scan_force_current_reimport, {})
autosnippets.setup() autosnippets.setup()
end end

View File

@@ -1,5 +1,6 @@
local ls = require('luasnip') local ls = require('luasnip')
local d = ls.dynamic_node local d = ls.dynamic_node
local i = ls.insert_node
local s = ls.snippet_node local s = ls.snippet_node
local t = ls.text_node local t = ls.text_node
local helper = require('typstar.autosnippets') local helper = require('typstar.autosnippets')
@@ -8,15 +9,15 @@ local cap = helper.cap
local math = helper.in_math local math = helper.in_math
local markup = helper.in_markup local markup = helper.in_markup
local letter_snippets = {}
local greek_letters_map = { local greek_letters_map = {
['a'] = 'alpha', ['a'] = 'alpha',
['b'] = 'beta', ['b'] = 'beta',
['c'] = 'chi', ['c'] = 'chi',
['d'] = 'delta', ['d'] = 'delta',
['e'] = 'epsilon', ['e'] = 'epsilon',
['f'] = 'phi',
['g'] = 'gamma', ['g'] = 'gamma',
['h'] = 'phi', ['h'] = 'eta',
['i'] = 'iotta', ['i'] = 'iotta',
['j'] = 'theta', ['j'] = 'theta',
['k'] = 'kappa', ['k'] = 'kappa',
@@ -41,6 +42,8 @@ local common_indices = { '\\d+', '[i-n]' }
-- buitins and caligraphic letters from github.com/lentilus/readable-typst -- buitins and caligraphic letters from github.com/lentilus/readable-typst
local index_conflicts = { 'in', 'ln', 'pi', 'xi', 'Ii', 'Jj', 'Kk', 'Ll', 'Mm', 'Nn' } local index_conflicts = { 'in', 'ln', 'pi', 'xi', 'Ii', 'Jj', 'Kk', 'Ll', 'Mm', 'Nn' }
local index_conflicts_set = {} local index_conflicts_set = {}
local punctuation_prepend_space = { ',', ';' }
local punctuation_prepend_space_set = {}
local trigger_greek = '' local trigger_greek = ''
local trigger_index_pre = '' local trigger_index_pre = ''
local trigger_index_post = '' local trigger_index_post = ''
@@ -59,9 +62,14 @@ for latin, greek in pairs(greek_letters_map) do
table.insert(greek_keys, latin:upper()) table.insert(greek_keys, latin:upper())
end end
for _, conflict in ipairs(index_conflicts) do local generate_bool_set = function(arr, target)
index_conflicts_set[conflict] = true for _, val in ipairs(arr) do
target[val] = true
end end
end
generate_bool_set(index_conflicts, index_conflicts_set)
generate_bool_set(punctuation_prepend_space, punctuation_prepend_space_set)
greek_letters_map = greek_full greek_letters_map = greek_full
trigger_greek = table.concat(greek_keys, '|') trigger_greek = table.concat(greek_keys, '|')
@@ -70,31 +78,62 @@ trigger_index_post = table.concat(common_indices, '|')
local get_greek = function(_, snippet) return s(nil, t(greek_letters_map[snippet.captures[1]])) end local get_greek = function(_, snippet) return s(nil, t(greek_letters_map[snippet.captures[1]])) end
local get_index = function(_, snippet) local get_index = function(_, snippet, _, idx1, idx2)
local letter, index = snippet.captures[1], snippet.captures[2] local letter, index = snippet.captures[idx1], snippet.captures[idx2]
local trigger = letter .. index local trigger = letter .. index
if index_conflicts_set[trigger] then return s(nil, t(trigger)) end if index_conflicts_set[trigger] then return s(nil, t(trigger)) end
return s(nil, t(letter .. '_' .. index)) return s(nil, t(letter .. '_' .. index))
end end
table.insert(letter_snippets, snip(':([A-Za-z0-9])', '$<>$ ', { cap(1) }, markup)) local get_series = function(_, snippet)
table.insert(letter_snippets, snip(';(' .. trigger_greek .. ')', '$<>$ ', { d(1, get_greek) }, markup)) local letter, target = snippet.captures[1], snippet.captures[2]
table.insert(letter_snippets, snip(';(' .. trigger_greek .. ')', '<>', { d(1, get_greek) }, math)) local target_num = tonumber(target)
table.insert( local result
letter_snippets, if target_num then
snip( local res = {}
'\\$(' .. trigger_index_pre .. ')\\$' .. '(' .. trigger_index_post .. ') ', for n = 1, target_num do
'$<>$ ', table.insert(res, string.format('%s_%d', letter, n))
{ d(1, get_index) }, if n ~= target_num then table.insert(res, ', ') end
markup, end
500 result = table.concat(res, '')
) else
) result = string.format('%s_1, %s_2, ..., %s_%s', letter, letter, letter, target)
table.insert( end
letter_snippets, return s(nil, t(result))
snip('(' .. trigger_index_pre .. ')' .. '(' .. trigger_index_post .. ') ', '<> ', { d(1, get_index) }, math, 200) end
)
local prepend_space = function(_, snippet, _, idx)
local punc = snippet.captures[idx]
if punctuation_prepend_space_set[punc] then punc = punc .. ' ' end
return s(nil, t(punc))
end
return { return {
unpack(letter_snippets), -- latin/greek
snip(':([A-Za-z0-9])', '$<>$ ', { cap(1) }, markup),
snip(';(' .. trigger_greek .. ')', '$<>$ ', { d(1, get_greek) }, markup),
snip(';(' .. trigger_greek .. ')', '<>', { d(1, get_greek) }, math),
-- indices
snip(
'\\$(' .. trigger_index_pre .. ')\\$' .. '(' .. trigger_index_post .. ')([^\\w])',
'$<>$<>',
{ d(1, get_index, {}, { user_args = { 1, 2 } }), d(2, prepend_space, {}, { user_args = { 3 } }) },
markup,
500
),
snip(
'(' .. trigger_index_pre .. ')' .. '(' .. trigger_index_post .. ')([^\\w])',
'<><>',
{ d(1, get_index, {}, { user_args = { 1, 2 } }), d(2, prepend_space, {}, { user_args = { 3 } }) },
math,
200
),
-- series of numbered letters
snip('(' .. trigger_index_pre .. ') ot ', '<>_1, <>_2, ... ', { cap(1), cap(1) }, math), -- a_1, a_2, ...
snip('(' .. trigger_index_pre .. ') ot(\\w+) ', '<> ', { d(1, get_series) }, math), -- a_1, a_2, ... a_j or a_1, a_2, a_2, a_3, a_4, a_5
-- misc
snip('(' .. trigger_index_pre .. ')bl', 'B_<> (<>)', { cap(1), i(1, 'x_0') }, math),
} }

View File

@@ -36,26 +36,35 @@ return {
-- operators -- operators
snip('ak([^k ])', '+ <>', { cap(1) }, math, 100, false), snip('ak([^k ])', '+ <>', { cap(1) }, math, 100, false),
snip('sk([^k ])', '- <>', { cap(1) }, math, 100, false), snip('sk([^k ])', '- <>', { cap(1) }, math, 100, false),
snip('oak', 'plus.circle ', {}, math, 1100), snip('oak', 'plus.circle ', {}, math),
snip('bak', 'plus.square ', {}, math, 1100), snip('bak', 'plus.square ', {}, math),
snip('mak', 'plus.minus ', {}, math, 1100), snip('mak', 'plus.minus ', {}, math),
snip('xx', 'times ', {}, math), snip('xx', 'times ', {}, math, 900),
snip('oxx', 'times.circle ', {}, math), snip('oxx', 'times.circle ', {}, math),
snip('bxx', 'times.square ', {}, math), snip('bxx', 'times.square ', {}, math),
snip('ff', '(<>) / (<>) <>', { i(1, 'a'), i(2, 'b'), i(3) }, math),
-- exponents
snip('iv', '^(-1) ', {}, math, 500, false),
snip('sr', '^2 ', {}, math, 500, false),
snip('cb', '^3 ', {}, math, 500, false),
snip('jj', '_(<>) ', { i(1, 'n') }, math, 500, false),
snip('kk', '^(<>) ', { i(1, 'n') }, math, 500, false),
snip('ep', 'exp(<>) ', { i(1, '1') }, math),
-- sets -- sets
-- 'st' to '{<>} in ./visual.lua -- 'st' to '{<>} in ./visual.lua
snip('set', '{<> | <>}', { i(1), i(2) }, math), snip('set', '{<> | <>}', { i(1), i(2) }, math),
snip('es', 'emptyset ', {}, math), snip('es', 'emptyset ', {}, math, 900),
snip('ses', '{emptyset} ', {}, math), snip('ses', '{emptyset} ', {}, math),
snip('sp', 'supset ', {}, math), snip('sp', 'supset ', {}, math),
snip('sb', 'subset ', {}, math), snip('sb', 'subset ', {}, math),
snip('sep', 'supset.eq ', {}, math), snip('sep', 'supset.eq ', {}, math),
snip('seb', 'subset.eq ', {}, math), snip('seb', 'subset.eq ', {}, math),
snip('nn', 'sect ', {}, math), snip('nn', 'sect ', {}, math, 900),
snip('uu', 'union ', {}, math), snip('uu', 'union ', {}, math, 900),
snip('bnn', 'sect.big ', {}, math, 1100), snip('bnn', 'sect.big ', {}, math),
snip('buu', 'union.big ', {}, math, 1100), snip('buu', 'union.big ', {}, math),
snip('swo', 'without ', {}, math), snip('swo', 'without ', {}, math),
-- misc -- misc
@@ -63,28 +72,22 @@ return {
snip('mt', '|->> ', {}, math), snip('mt', '|->> ', {}, math),
snip('Oo', 'compose ', {}, math), snip('Oo', 'compose ', {}, math),
snip('iso', 'tilde.equiv ', {}, math), snip('iso', 'tilde.equiv ', {}, math),
snip('ep', 'exp(<>) ', { i(1, '1') }, math),
snip('cc', 'cases(\n\t<>\n)\\', { i(1, '1') }, math), snip('cc', 'cases(\n\t<>\n)\\', { i(1, '1') }, math),
snip('(K|M|N|Q|R|S|Z)([\\dn]) ', '<><>^<> ', { cap(1), cap(1), cap(2) }, math), snip('(K|M|N|Q|R|S|Z)([\\dn]) ', '<><>^<> ', { cap(1), cap(1), cap(2) }, math),
snip('(.*)iv', '<>^(-1)', { cap(1) }, math),
snip('(.*)sr', '<>^2', { cap(1) }, math),
snip('(.*)cb', '<>^3', { cap(1) }, math),
snip('(.*)jj', '<>_(<>)', { cap(1), i(1, 'n') }, math),
snip('(.*)kk', '<>^(<>)', { cap(1), i(1, 'n') }, math),
snip('ddx', '(d <>)(d <>) ', { i(1, 'f'), i(2, 'x') }, math), snip('ddx', '(d <>)(d <>) ', { i(1, 'f'), i(2, 'x') }, math),
snip('it', 'integral', {}, math), snip('it', 'integral ', {}, math, 900),
snip('int', 'integral_(<>)^(<>) ', { i(1, 'a'), i(2, 'b') }, math), snip('int', 'integral_(<>)^(<>) ', { i(1, 'a'), i(2, 'b') }, math),
snip('oit', 'integral_Omega ', {}, math), snip('oit', 'integral_Omega ', {}, math),
snip('dit', 'integral_(<>) ', { i(1, 'Omega') }, math), snip('dit', 'integral_(<>) ', { i(1, 'Omega') }, math),
snip('sm', 'sum ', {}, math), snip('sm', 'sum ', {}, math, 900),
snip('sum', 'sum_(<>)^(<>) ', { i(1, 'i=0'), i(2, 'oo') }, math), snip('sum', 'sum_(<>)^(<>) ', { i(1, 'i=0'), i(2, 'oo') }, math),
snip('osm', 'sum_Omega ', {}, math), snip('osm', 'sum_Omega ', {}, math),
snip('dsm', 'sum_(<>) ', { i(1, 'I') }, math), snip('dsm', 'sum_(<>) ', { i(1, 'I') }, math),
snip('lm', 'lim <>', { i(1, 'a_n') }, math), snip('lm', 'lim ', {}, math),
snip('lim', 'lim_(<> ->> <>) <>', { i(1, 'n'), i(2, 'oo'), i(3, 'a_n') }, math), snip('lim', 'lim_(<> ->> <>) ', { i(1, 'n'), i(2, 'oo') }, math),
snip('lim (sup|inf)', 'lim<> <>', { cap(1), i(1, 'a_n') }, math), snip('lim (sup|inf)', 'lim<> ', { cap(1) }, math),
snip('lim(_.*-.*) (sup|inf)', 'lim<><> <>', { cap(2), cap(1), i(1, 'a_n') }, math), snip('lim(_\\(\\s?\\w+\\s?->\\s?\\w+\\s?\\)) (sup|inf)', 'lim<><> ', { cap(2), cap(1) }, math),
} }

View File

@@ -81,7 +81,7 @@ local smart_wrap = function(args, parent, old_state, expand)
end end
for _, val in pairs(operations) do for _, val in pairs(operations) do
table.insert(snippets, snip(val[1], '<>', { d(1, smart_wrap, {}, { user_args = { val } }) }, math, 1000, false)) table.insert(snippets, snip(val[1], '<>', { d(1, smart_wrap, {}, { user_args = { val } }) }, math, 1500, false))
end end
return { return {

View File

@@ -46,6 +46,11 @@ function M.run_shell_command(cmd, show_output)
end end
end end
function M.count_string(str, tocount)
local _, count = str:gsub(tocount, '')
return count
end
function M.char_to_hex(c) return string.format('%%%02X', string.byte(c)) end function M.char_to_hex(c) return string.format('%%%02X', string.byte(c)) end
function M.urlencode(url) function M.urlencode(url)

View File

@@ -8,7 +8,7 @@ import aiohttp
from .flashcard import Flashcard from .flashcard import Flashcard
async def _gather_exceptions(coroutines): async def gather_exceptions(coroutines):
for result in await asyncio.gather(*coroutines, return_exceptions=True): for result in await asyncio.gather(*coroutines, return_exceptions=True):
if isinstance(result, Exception): if isinstance(result, Exception):
raise result raise result
@@ -28,7 +28,7 @@ class AnkiConnectApi:
self.api_key = api_key self.api_key = api_key
self.semaphore = asyncio.Semaphore(2) # increase in case Anki implements multithreading self.semaphore = asyncio.Semaphore(2) # increase in case Anki implements multithreading
async def push_flashcards(self, cards: Iterable[Flashcard]): async def push_flashcards(self, cards: Iterable[Flashcard], reimport: bool):
add: dict[str, List[Flashcard]] = defaultdict(list) add: dict[str, List[Flashcard]] = defaultdict(list)
update: dict[str, List[Flashcard]] = defaultdict(list) update: dict[str, List[Flashcard]] = defaultdict(list)
n_add: int = 0 n_add: int = 0
@@ -41,6 +41,14 @@ class AnkiConnectApi:
else: else:
update[card.deck].append(card) update[card.deck].append(card)
n_update += 1 n_update += 1
if reimport:
reimport_cards = await self._check_reimport(update)
print(f"Found {len(reimport_cards)} flashcards to reimport")
for card in reimport_cards:
update[card.deck].remove(card)
add[card.deck].append(card)
n_update -= 1
n_add += 1
print( print(
f"Pushing {n_add} new flashcards and {n_update} updated flashcards to Anki...", f"Pushing {n_add} new flashcards and {n_update} updated flashcards to Anki...",
@@ -48,7 +56,7 @@ class AnkiConnectApi:
) )
await self._create_required_decks({*add.keys(), *update.keys()}) await self._create_required_decks({*add.keys(), *update.keys()})
await self._add_new_cards(add) await self._add_new_cards(add)
await _gather_exceptions( await gather_exceptions(
[ [
*self._update_cards_requests(add), *self._update_cards_requests(add),
*self._update_cards_requests(update, True), *self._update_cards_requests(update, True),
@@ -115,7 +123,18 @@ class AnkiConnectApi:
for deck in required: for deck in required:
if deck not in existing: if deck not in existing:
requests.append(self._request_api("createDeck", deck=deck)) requests.append(self._request_api("createDeck", deck=deck))
await _gather_exceptions(requests) await gather_exceptions(requests)
async def _check_reimport(self, cards_map: dict[str, List[Flashcard]]) -> List[Flashcard]:
cards = []
for cs in cards_map.values():
cards.extend(cs)
if not cards:
return []
existing = await self._request_api(
"findNotes", query=f"nid:{','.join([str(c.note_id) for c in cards])}"
)
return [c for c in cards if c.note_id not in existing]
def _update_cards_requests( def _update_cards_requests(
self, cards_map: dict[str, List[Flashcard]], update_deck: bool = True self, cards_map: dict[str, List[Flashcard]], update_deck: bool = True

View File

@@ -78,7 +78,7 @@ class Flashcard:
self.back_node = back self.back_node = back
self.note_id_node = note_id self.note_id_node = note_id
def update_id(self, value): def update_id(self, value: int):
if self.note_id != value: if self.note_id != value:
self.note_id = value self.note_id = value
self.id_updated = True self.id_updated = True

View File

@@ -12,7 +12,9 @@ from anki.typst_compiler import TypstCompiler
cli = typer.Typer(name="typstar-anki") cli = typer.Typer(name="typstar-anki")
async def export_flashcards(root_dir, force_scan, clear_cache, typst_cmd, anki_url, anki_key): async def export_flashcards(
root_dir, force_scan, clear_cache, reimport, typst_cmd, anki_url, anki_key
):
parser = FlashcardParser() parser = FlashcardParser()
compiler = TypstCompiler(root_dir, typst_cmd) compiler = TypstCompiler(root_dir, typst_cmd)
api = AnkiConnectApi(anki_url, anki_key) api = AnkiConnectApi(anki_url, anki_key)
@@ -27,7 +29,7 @@ async def export_flashcards(root_dir, force_scan, clear_cache, typst_cmd, anki_u
try: try:
# async anki push # async anki push
await api.push_flashcards(flashcards) await api.push_flashcards(flashcards, reimport)
finally: finally:
# write id updates to files # write id updates to files
parser.update_ids_in_source() parser.update_ids_in_source()
@@ -57,13 +59,24 @@ def cmd(
"as it clears hashes regardless of their path)" "as it clears hashes regardless of their path)"
), ),
] = False, ] = False,
reimport: Annotated[
bool,
typer.Option(
help="Instead of throwing an error also add flashcards that have already been asigned an id"
"but are not present in Anki. The asigned id will be updated."
),
] = False,
typst_cmd: Annotated[ typst_cmd: Annotated[
str, typer.Option(help="Typst command used for flashcard compilation") str, typer.Option(help="Typst command used for flashcard compilation")
] = "typst", ] = "typst",
anki_url: Annotated[str, typer.Option(help="Url for Anki-Connect")] = "http://127.0.0.1:8765", anki_url: Annotated[str, typer.Option(help="Url for Anki-Connect")] = "http://127.0.0.1:8765",
anki_key: Annotated[str | None, typer.Option(help="Api key for Anki-Connect")] = None, anki_key: Annotated[str | None, typer.Option(help="Api key for Anki-Connect")] = None,
): ):
asyncio.run(export_flashcards(root_dir, force_scan, clear_cache, typst_cmd, anki_url, anki_key)) asyncio.run(
export_flashcards(
root_dir, force_scan, clear_cache, reimport, typst_cmd, anki_url, anki_key
)
)
def main(): def main():