commit dcb7debff6ffdc054a53247d6272cf0e926e4186 Author: Barrett Ruth Date: Thu Sep 11 23:52:32 2025 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..297e1e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv/ +venv/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/after/ftdetect/cp.lua b/after/ftdetect/cp.lua new file mode 100644 index 0000000..297c359 --- /dev/null +++ b/after/ftdetect/cp.lua @@ -0,0 +1,13 @@ +vim.api.nvim_create_autocmd({ 'BufRead', 'BufNewFile' }, { + pattern = '*/io/*.in', + callback = function() + vim.bo.filetype = 'cpinput' + end, +}) + +vim.api.nvim_create_autocmd({ 'BufRead', 'BufNewFile' }, { + pattern = '*/io/*.out', + callback = function() + vim.bo.filetype = 'cpoutput' + end, +}) diff --git a/after/ftplugin/cpinput.lua b/after/ftplugin/cpinput.lua new file mode 100644 index 0000000..622ad6a --- /dev/null +++ b/after/ftplugin/cpinput.lua @@ -0,0 +1,6 @@ +vim.opt_local.number = false +vim.opt_local.relativenumber = false +vim.opt_local.statuscolumn = '' +vim.opt_local.signcolumn = 'no' +vim.opt_local.wrap = true +vim.opt_local.linebreak = true diff --git a/after/ftplugin/cpoutput.lua b/after/ftplugin/cpoutput.lua new file mode 100644 index 0000000..622ad6a --- /dev/null +++ b/after/ftplugin/cpoutput.lua @@ -0,0 +1,6 @@ +vim.opt_local.number = false +vim.opt_local.relativenumber = false +vim.opt_local.statuscolumn = '' +vim.opt_local.signcolumn = 'no' +vim.opt_local.wrap = true +vim.opt_local.linebreak = true diff --git a/after/syntax/cpoutput.vim b/after/syntax/cpoutput.vim new file mode 100644 index 0000000..28092a7 --- /dev/null +++ b/after/syntax/cpoutput.vim @@ -0,0 +1,17 @@ +if exists("b:current_syntax") + finish +endif + +syntax match cpOutputCode /^\[code\]: .*/ +syntax match cpOutputTime /^\[time\]: .*/ +syntax match cpOutputDebug /^\[debug\]: .*/ +syntax match cpOutputMatchesTrue /^\[matches\]: true$/ +syntax match cpOutputMatchesFalse /^\[matches\]: false$/ + +highlight default link cpOutputCode DiagnosticInfo +highlight default link cpOutputTime Comment +highlight default link cpOutputDebug Comment +highlight default link cpOutputMatchesTrue DiffAdd +highlight default link cpOutputMatchesFalse DiffDelete + +let b:current_syntax = "cpoutput" \ No newline at end of file diff --git a/lua/cp/config.lua b/lua/cp/config.lua new file mode 100644 index 0000000..0dcface --- /dev/null +++ b/lua/cp/config.lua @@ -0,0 +1,19 @@ +local M = {} + +M.defaults = { + template_dir = nil, + contests = { + atcoder = { cpp_version = 23 }, + codeforces = { cpp_version = 23 }, + cses = { cpp_version = 20 }, + icpc = { cpp_version = 20 }, + usaco = { cpp_version = 17 }, + }, + snippets = {}, +} + +function M.setup(user_config) + return vim.tbl_deep_extend('force', M.defaults, user_config or {}) +end + +return M diff --git a/lua/cp/init.lua b/lua/cp/init.lua new file mode 100644 index 0000000..8caadfd --- /dev/null +++ b/lua/cp/init.lua @@ -0,0 +1,344 @@ +local config_module = require('cp.config') +local snippets = require('cp.snippets') + +local M = {} +local config = {} + +local function log(msg, level) + vim.notify(('[cp.nvim]: %s'):format(msg), level or vim.log.levels.INFO) +end + +local function clearcol() + vim.api.nvim_set_option_value('number', false, { scope = 'local' }) + vim.api.nvim_set_option_value('relativenumber', false, { scope = 'local' }) + vim.api.nvim_set_option_value('statuscolumn', '', { scope = 'local' }) + vim.api.nvim_set_option_value('signcolumn', 'no', { scope = 'local' }) + vim.api.nvim_set_option_value('equalalways', false, { scope = 'global' }) +end + +local function get_plugin_path() + local plugin_path = debug.getinfo(1, 'S').source:sub(2) + return vim.fn.fnamemodify(plugin_path, ':h:h:h') +end + +local function setup_python_env() + local plugin_path = get_plugin_path() + local venv_dir = plugin_path .. '/.venv' + + if vim.fn.executable('uv') == 0 then + log( + 'uv is not installed. Install it to enable problem scraping: https://docs.astral.sh/uv/', + vim.log.levels.WARN + ) + return false + end + + if vim.fn.isdirectory(venv_dir) == 0 then + log('Setting up Python environment for scrapers...') + local result = vim.fn.system( + ('cd %s && uv sync'):format(vim.fn.shellescape(plugin_path)) + ) + if vim.v.shell_error ~= 0 then + log( + 'Failed to setup Python environment: ' .. result, + vim.log.levels.ERROR + ) + return false + end + log('Python environment setup complete') + end + + return true +end + +local competition_types = { 'atcoder', 'codeforces', 'cses', 'icpc' } + +local function setup_contest(contest_type) + if not vim.tbl_contains(competition_types, contest_type) then + log( + ('Unknown contest type. Available: [%s]'):format( + table.concat(competition_types, ', ') + ), + vim.log.levels.ERROR + ) + return + end + + vim.g.cp_contest = contest_type + vim.fn.system(('cp -fr %s/* .'):format(config.template_dir)) + vim.fn.system( + ('make setup VERSION=%s'):format( + config.contests[contest_type].cpp_version + ) + ) + log(('set up %s contest environment'):format(contest_type)) +end + +local function setup_problem(problem_id, problem_letter) + if not vim.g.cp_contest then + log( + 'no contest mode set. run :CP first', + vim.log.levels.ERROR + ) + return + end + + if vim.g.cp_diff_mode then + vim.cmd.diffoff() + if vim.g.cp_saved_session then + vim.fn.delete(vim.g.cp_saved_session) + vim.g.cp_saved_session = nil + end + if vim.g.cp_temp_output then + vim.fn.delete(vim.g.cp_temp_output) + vim.g.cp_temp_output = nil + end + vim.g.cp_diff_mode = false + end + + vim.cmd.only() + + local filename, full_problem_id + if + (vim.g.cp_contest == 'atcoder' or vim.g.cp_contest == 'codeforces') + and problem_letter + then + full_problem_id = problem_id .. problem_letter + filename = full_problem_id .. '.cc' + vim.fn.system( + ('make scrape %s %s %s'):format( + vim.g.cp_contest, + problem_id, + problem_letter + ) + ) + else + full_problem_id = problem_id + filename = problem_id .. '.cc' + vim.fn.system( + ('make scrape %s %s'):format(vim.g.cp_contest, problem_id) + ) + end + + vim.cmd.e(filename) + + if vim.api.nvim_buf_get_lines(0, 0, -1, true)[1] == '' then + vim.api.nvim_input(('i%s'):format(vim.g.cp_contest)) + end + + vim.api.nvim_set_option_value('winbar', '', { scope = 'local' }) + vim.api.nvim_set_option_value('foldlevel', 0, { scope = 'local' }) + vim.api.nvim_set_option_value('foldmethod', 'marker', { scope = 'local' }) + vim.api.nvim_set_option_value('foldmarker', '{{{,}}}', { scope = 'local' }) + vim.api.nvim_set_option_value('foldtext', '', { scope = 'local' }) + + vim.diagnostic.enable(false) + + local base_fp = vim.fn.fnamemodify(filename, ':p:h') + local input = ('%s/io/%s.in'):format(base_fp, full_problem_id) + local output = ('%s/io/%s.out'):format(base_fp, full_problem_id) + + vim.cmd.vsplit(output) + vim.cmd.w() + clearcol() + vim.cmd(('vertical resize %d'):format(math.floor(vim.o.columns * 0.3))) + vim.cmd.split(input) + vim.cmd.w() + clearcol() + vim.cmd.wincmd('h') + + log(('switched to problem %s'):format(full_problem_id)) +end + +local function get_current_problem() + local filename = vim.fn.expand('%:t:r') + if filename == '' then + log('no file open', vim.log.levels.ERROR) + return nil + end + return filename +end + +local function run_problem() + local problem_id = get_current_problem() + if not problem_id then + return + end + + local has_lsp, lsp = pcall(require, 'lsp') + if has_lsp and lsp.lsp_format then + lsp.lsp_format({ async = true }) + end + + vim.system({ 'make', 'run', vim.fn.expand('%:t') }, {}, function() + vim.schedule(function() + vim.cmd.checktime() + end) + end) +end + +local function debug_problem() + local problem_id = get_current_problem() + if not problem_id then + return + end + + local has_lsp, lsp = pcall(require, 'lsp') + if has_lsp and lsp.lsp_format then + lsp.lsp_format({ async = true }) + end + + vim.system({ 'make', 'debug', vim.fn.expand('%:t') }, {}, function() + vim.schedule(function() + vim.cmd.checktime() + end) + end) +end + +local function diff_problem() + if vim.g.cp_diff_mode then + vim.cmd.diffoff() + if vim.g.cp_saved_session then + vim.cmd(('silent! source %s'):format(vim.g.cp_saved_session)) + vim.fn.delete(vim.g.cp_saved_session) + vim.g.cp_saved_session = nil + end + vim.g.cp_diff_mode = false + log('exited diff mode') + else + local problem_id = get_current_problem() + if not problem_id then + return + end + + local base_fp = vim.fn.getcwd() + local output = ('%s/io/%s.out'):format(base_fp, problem_id) + local expected = ('%s/io/%s.expected'):format(base_fp, problem_id) + local input = ('%s/io/%s.in'):format(base_fp, problem_id) + + if vim.fn.filereadable(expected) == 0 then + log( + ('No expected output file found: %s'):format(expected), + vim.log.levels.ERROR + ) + return + end + + local temp_output = vim.fn.tempname() + vim.fn.system( + ("awk '/^\\[[^]]*\\]:/ {exit} {print}' %s > %s"):format( + vim.fn.shellescape(output), + temp_output + ) + ) + + local session_file = vim.fn.tempname() .. '.vim' + vim.cmd(('silent! mksession! %s'):format(session_file)) + vim.g.cp_saved_session = session_file + + vim.cmd.diffoff() + vim.cmd.only() + + vim.cmd.edit(temp_output) + vim.cmd.diffthis() + clearcol() + + vim.cmd.vsplit(expected) + vim.cmd.diffthis() + clearcol() + + vim.cmd(('botright split %s'):format(input)) + clearcol() + vim.cmd.wincmd('k') + + vim.g.cp_diff_mode = true + vim.g.cp_temp_output = temp_output + log('entered diff mode') + end +end + +local initialized = false + +function M.setup(user_config) + if initialized and not user_config then + return + end + + config = config_module.setup(user_config) + + local plugin_path = get_plugin_path() + config.template_dir = plugin_path .. '/templates' + config.snippets.path = plugin_path .. '/templates/snippets' + + snippets.setup(config) + + if initialized then + return + end + initialized = true + + setup_python_env() + + vim.api.nvim_create_user_command('CP', function(opts) + local args = opts.fargs + if #args == 0 then + log( + 'Usage: :CP ', + vim.log.levels.ERROR + ) + return + end + + local cmd = args[1] + + if vim.tbl_contains(competition_types, cmd) then + if args[2] then + setup_contest(cmd) + if (cmd == 'atcoder' or cmd == 'codeforces') and args[3] then + setup_problem(args[2], args[3]) + else + setup_problem(args[2]) + end + else + setup_contest(cmd) + end + elseif cmd == 'run' then + run_problem() + elseif cmd == 'debug' then + debug_problem() + elseif cmd == 'diff' then + diff_problem() + else + if vim.g.cp_contest then + if + ( + vim.g.cp_contest == 'atcoder' + or vim.g.cp_contest == 'codeforces' + ) and args[2] + then + setup_problem(cmd, args[2]) + else + setup_problem(cmd) + end + else + log( + 'no contest mode set. run :CP first or use full command', + vim.log.levels.ERROR + ) + end + end + end, { + nargs = '*', + complete = function(ArgLead, CmdLine, ...) + local commands = vim.list_extend( + vim.deepcopy(competition_types), + { 'run', 'debug', 'diff' } + ) + return vim.tbl_filter(function(cmd) + return cmd:find(ArgLead, 1, true) == 1 + end, commands) + end, + }) +end + +return M diff --git a/lua/cp/snippets.lua b/lua/cp/snippets.lua new file mode 100644 index 0000000..73ed47d --- /dev/null +++ b/lua/cp/snippets.lua @@ -0,0 +1,24 @@ +local M = {} + +function M.setup(config) + local has_luasnip, luasnip = pcall(require, 'luasnip') + if not has_luasnip then + return + end + + local snippets = {} + + for name, snippet in pairs(config.snippets or {}) do + if type(snippet) == 'table' and snippet.trig then + table.insert(snippets, snippet) + else + table.insert(snippets, snippet) + end + end + + if #snippets > 0 then + luasnip.add_snippets('cpp', snippets) + end +end + +return M diff --git a/plugin/cp.lua b/plugin/cp.lua new file mode 100644 index 0000000..7ed2c91 --- /dev/null +++ b/plugin/cp.lua @@ -0,0 +1,6 @@ +if vim.g.loaded_cp then + return +end +vim.g.loaded_cp = 1 + +require('cp').setup() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6bd65d5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "scrapers" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "beautifulsoup4>=4.13.5", + "cloudscraper>=1.2.71", + "requests>=2.32.5", +] diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..fce58e9 --- /dev/null +++ b/readme.md @@ -0,0 +1,42 @@ +# cp.nvim + +A Neovim plugin for competitive programming. + +## Features + +- Support for multiple online judges (AtCoder, Codeforces, CSES, ICPC) +- Automatic problem scraping and test case management +- Integrated build, run, and debug commands +- Diff mode for comparing output with expected results +- LuaSnip integration for contest-specific snippets + +## Requirements + +- Neovim 0.9+ +- `make` and a C++ compiler +- (Optional) [uv](https://docs.astral.sh/uv/) for problem scraping +- (Optional) [LuaSnip](https://github.com/L3MON4D3/LuaSnip) for snippets + +## Installation + +Using [lazy.nvim](https://github.com/folke/lazy.nvim): + +```lua +{ + "barrett-ruth/cp.nvim", + cmd = "CP", + dependencies = { + "L3MON4D3/LuaSnip", + } +} +``` + +## Documentation + +```vim +:help cp.nvim +``` + +## TODO + +- USACO support diff --git a/selene.toml b/selene.toml new file mode 100644 index 0000000..a286987 --- /dev/null +++ b/selene.toml @@ -0,0 +1,6 @@ +std = "vim" + +exclude = [".luacheckrc"] + +[rules] +mixed_table = "allow" diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..dda733b --- /dev/null +++ b/stylua.toml @@ -0,0 +1,4 @@ +quote_style = "AutoPreferSingle" +indent_type = "Spaces" +column_width = 80 +collapse_simple_statement = "Never" diff --git a/templates/.clang-format b/templates/.clang-format new file mode 100644 index 0000000..e7350c4 --- /dev/null +++ b/templates/.clang-format @@ -0,0 +1,9 @@ +BasedOnStyle: Google +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortCompoundRequirementOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLambdasOnASingleLine: false +AllowShortLoopsOnASingleLine: false diff --git a/templates/.clangd b/templates/.clangd new file mode 100644 index 0000000..e5046e6 --- /dev/null +++ b/templates/.clangd @@ -0,0 +1,33 @@ +CompileFlags: + Add: + -O2 + -Wall + -Wextra + -Wpedantic + -Wshadow + -Wformat=2 + -Wfloat-equal + -Wlogical-op + -Wshift-overflow=2 + -Wnon-virtual-dtor + -Wold-style-cast + -Wcast-qual + -Wuseless-cast + -Wno-sign-promotion + -Wcast-align + -Wunused + -Woverloaded-virtual + -Wconversion + -Wsign-conversion + -Wmisleading-indentation + -Wduplicated-cond + -Wduplicated-branches + -Wlogical-op + -Wnull-dereference + -Wformat=2 + -Wformat-overflow + -Wformat-truncation + -Wdouble-promotion + -Wundef + -DLOCAL + -Wno-unknown-pragmas diff --git a/templates/compile_flags.txt b/templates/compile_flags.txt new file mode 100644 index 0000000..04b1b00 --- /dev/null +++ b/templates/compile_flags.txt @@ -0,0 +1,2 @@ +-O2 +-DLOCAL diff --git a/templates/debug_flags.txt b/templates/debug_flags.txt new file mode 100644 index 0000000..a2c29c5 --- /dev/null +++ b/templates/debug_flags.txt @@ -0,0 +1,3 @@ +-g3 +-fsanitize=address,undefined +-DLOCAL diff --git a/templates/makefile b/templates/makefile new file mode 100644 index 0000000..30acf70 --- /dev/null +++ b/templates/makefile @@ -0,0 +1,31 @@ +.PHONY: run debug clean setup init scrape + +VERSION ?= 20 + +SRC = $(word 2,$(MAKECMDGOALS)) + +.SILENT: + +run: + sh scripts/run.sh $(SRC) + +debug: + sh scripts/debug.sh $(SRC) + +clean: + rm -rf build/* + +setup: + test -d build || mkdir -p build + test -d io || mkdir -p io + test -f compile_flags.txt && echo -std=c++$(VERSION) >>compile_flags.txt + test -f .clangd && echo -e "\t\t-std=c++$(VERSION)" >>.clangd + +init: + make setup + +scrape: + sh scripts/scrape.sh $(word 2,$(MAKECMDGOALS)) $(word 3,$(MAKECMDGOALS)) $(word 4,$(MAKECMDGOALS)) + +%: + @: diff --git a/templates/scrapers/atcoder.py b/templates/scrapers/atcoder.py new file mode 100644 index 0000000..374ffdb --- /dev/null +++ b/templates/scrapers/atcoder.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +import sys + +import requests +from bs4 import BeautifulSoup + + +def parse_problem_url(contest_id: str, problem_letter: str) -> str: + task_id = f"{contest_id}_{problem_letter}" + return f"https://atcoder.jp/contests/{contest_id}/tasks/{task_id}" + + +def scrape(url: str) -> list[tuple[str, str]]: + try: + headers = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + + response = requests.get(url, headers=headers, timeout=10) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "html.parser") + + tests = [] + + sample_headers = soup.find_all( + "h3", string=lambda x: x and "sample" in x.lower() if x else False + ) + + i = 0 + while i < len(sample_headers): + header = sample_headers[i] + if "input" in header.get_text().lower(): + input_pre = header.find_next("pre") + if input_pre and i + 1 < len(sample_headers): + next_header = sample_headers[i + 1] + if "output" in next_header.get_text().lower(): + output_pre = next_header.find_next("pre") + if output_pre: + input_text = input_pre.get_text().strip().replace("\r", "") + output_text = ( + output_pre.get_text().strip().replace("\r", "") + ) + if input_text and output_text: + tests.append((input_text, output_text)) + i += 2 + continue + i += 1 + + return tests + + except Exception as e: + print(f"Error scraping AtCoder: {e}", file=sys.stderr) + return [] + + +def main(): + if len(sys.argv) != 3: + print("Usage: atcoder.py ", file=sys.stderr) + print("Example: atcoder.py abc042 a", file=sys.stderr) + sys.exit(1) + + contest_id = sys.argv[1] + problem_letter = sys.argv[2] + + url = parse_problem_url(contest_id, problem_letter) + print(f"Scraping: {url}", file=sys.stderr) + + tests = scrape(url) + + if not tests: + print(f"No tests found for {contest_id} {problem_letter}", file=sys.stderr) + sys.exit(1) + + print("---INPUT---") + print(len(tests)) + for input_data, output_data in tests: + print(input_data) + print("---OUTPUT---") + for input_data, output_data in tests: + print(output_data) + print("---END---") + + +if __name__ == "__main__": + main() diff --git a/templates/scrapers/codeforces.py b/templates/scrapers/codeforces.py new file mode 100644 index 0000000..ed31990 --- /dev/null +++ b/templates/scrapers/codeforces.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +import sys + +import cloudscraper +from bs4 import BeautifulSoup + + +def scrape(url: str): + try: + scraper = cloudscraper.create_scraper() + response = scraper.get(url, timeout=10) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "html.parser") + tests = [] + + input_sections = soup.find_all("div", class_="input") + output_sections = soup.find_all("div", class_="output") + + for inp_section, out_section in zip(input_sections, output_sections): + inp_pre = inp_section.find("pre") + out_pre = out_section.find("pre") + + if inp_pre and out_pre: + input_lines = [] + output_lines = [] + + for line_div in inp_pre.find_all("div", class_="test-example-line"): + input_lines.append(line_div.get_text().strip()) + + output_divs = out_pre.find_all("div", class_="test-example-line") + if not output_divs: + output_text_raw = out_pre.get_text().strip().replace("\r", "") + output_lines = [ + line.strip() + for line in output_text_raw.split("\n") + if line.strip() + ] + else: + for line_div in output_divs: + output_lines.append(line_div.get_text().strip()) + + if input_lines and output_lines: + if len(input_lines) > 1 and input_lines[0].isdigit(): + test_count = int(input_lines[0]) + remaining_input = input_lines[1:] + for i in range(min(test_count, len(output_lines))): + if i < len(remaining_input): + tests.append((remaining_input[i], output_lines[i])) + else: + input_text = "\n".join(input_lines) + output_text = "\n".join(output_lines) + tests.append((input_text, output_text)) + + return tests + + except Exception as e: + print(f"CloudScraper failed: {e}", file=sys.stderr) + return [] + + +def parse_problem_url(contest_id: str, problem_letter: str) -> str: + return ( + f"https://codeforces.com/contest/{contest_id}/problem/{problem_letter.upper()}" + ) + + +def scrape_sample_tests(url: str): + print(f"Scraping: {url}", file=sys.stderr) + return scrape(url) + + +def main(): + if len(sys.argv) != 3: + print("Usage: codeforces.py ", file=sys.stderr) + print("Example: codeforces.py 1234 A", file=sys.stderr) + sys.exit(1) + + contest_id = sys.argv[1] + problem_letter = sys.argv[2] + + url = parse_problem_url(contest_id, problem_letter) + tests = scrape_sample_tests(url) + + if not tests: + print(f"No tests found for {contest_id} {problem_letter}", file=sys.stderr) + print( + "Consider adding test cases manually to the io/ directory", file=sys.stderr + ) + sys.exit(1) + + print("---INPUT---") + print(len(tests)) + for input_data, output_data in tests: + print(input_data) + print("---OUTPUT---") + for input_data, output_data in tests: + print(output_data) + print("---END---") + + +if __name__ == "__main__": + main() diff --git a/templates/scrapers/cses.py b/templates/scrapers/cses.py new file mode 100755 index 0000000..38d43aa --- /dev/null +++ b/templates/scrapers/cses.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +import sys + +import requests +from bs4 import BeautifulSoup + + +def parse_problem_url(problem_input: str) -> str | None: + if problem_input.startswith("https://cses.fi/problemset/task/"): + return problem_input + elif problem_input.isdigit(): + return f"https://cses.fi/problemset/task/{problem_input}" + return None + + +def scrape(url: str) -> list[tuple[str, str]]: + try: + headers = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + + response = requests.get(url, headers=headers, timeout=10) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "html.parser") + + tests = [] + example_header = soup.find("h1", string="Example") + + if example_header: + current = example_header.find_next_sibling() + input_text = None + output_text = None + + while current: + if current.name == "p" and "Input:" in current.get_text(): + input_pre = current.find_next_sibling("pre") + if input_pre: + input_text = input_pre.get_text().strip() + elif current.name == "p" and "Output:" in current.get_text(): + output_pre = current.find_next_sibling("pre") + if output_pre: + output_text = output_pre.get_text().strip() + break + current = current.find_next_sibling() + + if input_text and output_text: + tests.append((input_text, output_text)) + + return tests + + except Exception as e: + print(f"Error scraping CSES: {e}", file=sys.stderr) + return [] + + +def main(): + if len(sys.argv) != 2: + print("Usage: cses.py ", file=sys.stderr) + sys.exit(1) + + problem_input = sys.argv[1] + url = parse_problem_url(problem_input) + + if not url: + print(f"Invalid problem input: {problem_input}", file=sys.stderr) + print("Use either problem ID (e.g., 1068) or full URL", file=sys.stderr) + sys.exit(1) + + tests = scrape(url) + + if not tests: + print(f"No tests found for {problem_input}", file=sys.stderr) + sys.exit(1) + + print("---INPUT---") + print(len(tests)) + for input_data, output_data in tests: + print(input_data) + print("---OUTPUT---") + for input_data, output_data in tests: + print(output_data) + print("---END---") + + +if __name__ == "__main__": + main() diff --git a/templates/scripts/debug.sh b/templates/scripts/debug.sh new file mode 100644 index 0000000..1e63f37 --- /dev/null +++ b/templates/scripts/debug.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +. ./scripts/utils.sh + +SRC="$1" +BASE=$(basename "$SRC" .cc) +INPUT="${BASE}.in" +OUTPUT="${BASE}.out" +DBG_BIN="${BASE}.debug" + +test -d build || mkdir -p build +test -d io || mkdir -p io + +test -f "$INPUT" && test ! -f "io/$INPUT" && mv "$INPUT" "io/" +test -f "$OUTPUT" && test ! -f "io/$OUTPUT" && mv "$OUTPUT" "io/" + +test -f "io/$INPUT" || touch "io/$INPUT" +test -f "io/$OUTPUT" || touch "io/$OUTPUT" + +INPUT="io/$INPUT" +OUTPUT="io/$OUTPUT" +DBG_BIN="build/$DBG_BIN" + +compile_source "$SRC" "$DBG_BIN" "$OUTPUT" @debug_flags.txt +CODE=$? +test $CODE -gt 0 && exit $CODE + +execute_binary "$DBG_BIN" "$INPUT" "$OUTPUT" true +exit $? diff --git a/templates/scripts/run.sh b/templates/scripts/run.sh new file mode 100644 index 0000000..ab9aa7d --- /dev/null +++ b/templates/scripts/run.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +. ./scripts/utils.sh + +SRC="$1" +BASE=$(basename "$SRC" .cc) +INPUT="${BASE}.in" +OUTPUT="${BASE}.out" +RUN_BIN="${BASE}.run" + +test -d build || mkdir -p build +test -d io || mkdir -p io + +test -f "$INPUT" && test ! -f "io/$INPUT" && mv "$INPUT" "io/" +test -f "$OUTPUT" && test ! -f "io/$OUTPUT" && mv "$OUTPUT" "io/" + +test -f "io/$INPUT" || touch "io/$INPUT" +test -f "io/$OUTPUT" || touch "io/$OUTPUT" + +INPUT="io/$INPUT" +OUTPUT="io/$OUTPUT" +RUN_BIN="build/$RUN_BIN" + +compile_source "$SRC" "$RUN_BIN" "$OUTPUT" "" +CODE=$? +test $CODE -gt 0 && exit $CODE + +execute_binary "$RUN_BIN" "$INPUT" "$OUTPUT" +exit $? diff --git a/templates/scripts/scrape.sh b/templates/scripts/scrape.sh new file mode 100755 index 0000000..973ef7b --- /dev/null +++ b/templates/scripts/scrape.sh @@ -0,0 +1,85 @@ +#!/bin/sh + +CONTEST="$1" +PROBLEM="$2" +PROBLEM_LETTER="$3" + +if [ -z "$CONTEST" ] || [ -z "$PROBLEM" ]; then + echo "Usage: make scrape [problem_letter]" + echo "Available contests: cses, atcoder, codeforces" + echo "Examples:" + echo " make scrape cses 1068" + echo " make scrape atcoder abc042 a" + echo " make scrape codeforces 1234 A" + exit +fi + +test -d io && true || mkdir -p io +TMPFILE=$(mktemp) +ORIGDIR=$(pwd) + +case "$CONTEST" in + cses) + cd "$(dirname "$0")/../.." && uv run scrapers/cses.py "$PROBLEM" > "$TMPFILE" + if [ $? -eq 0 ]; then + cd "$ORIGDIR" + awk '/^---INPUT---$/ {getline; while ($0 != "---OUTPUT---") {print; getline}} END {}' "$TMPFILE" > "io/$PROBLEM.in" + awk '/^---OUTPUT---$/ {getline; while ($0 != "---END---") {print; getline}} END {}' "$TMPFILE" > "io/$PROBLEM.expected" + echo "Scraped problem $PROBLEM to io/$PROBLEM.in and io/$PROBLEM.expected" + else + echo "Failed to scrape problem $PROBLEM" + cat "$TMPFILE" + rm "$TMPFILE" + exit + fi + ;; + atcoder) + if [ -z "$PROBLEM_LETTER" ]; then + echo "AtCoder requires problem letter (e.g., make scrape atcoder abc042 a)" + rm "$TMPFILE" + exit + fi + FULL_PROBLEM_ID="${PROBLEM}${PROBLEM_LETTER}" + cd "$(dirname "$0")/../.." && uv run scrapers/atcoder.py "$PROBLEM" "$PROBLEM_LETTER" > "$TMPFILE" + if [ $? -eq 0 ]; then + cd "$ORIGDIR" + awk '/^---INPUT---$/ {getline; while ($0 != "---OUTPUT---") {print; getline}} END {}' "$TMPFILE" > "io/$FULL_PROBLEM_ID.in" + awk '/^---OUTPUT---$/ {getline; while ($0 != "---END---") {print; getline}} END {}' "$TMPFILE" > "io/$FULL_PROBLEM_ID.expected" + echo "Scraped problem $FULL_PROBLEM_ID to io/$FULL_PROBLEM_ID.in and io/$FULL_PROBLEM_ID.expected" + else + echo "Failed to scrape problem $FULL_PROBLEM_ID" + cat "$TMPFILE" + rm "$TMPFILE" + exit + fi + ;; + codeforces) + if [ -z "$PROBLEM_LETTER" ]; then + echo "Codeforces requires problem letter (e.g., make scrape codeforces 1234 A)" + rm "$TMPFILE" + exit + fi + FULL_PROBLEM_ID="${PROBLEM}${PROBLEM_LETTER}" + cd "$(dirname "$0")/../.." && uv run scrapers/codeforces.py "$PROBLEM" "$PROBLEM_LETTER" > "$TMPFILE" + if [ $? -eq 0 ]; then + cd "$ORIGDIR" + awk '/^---INPUT---$/ {getline; while ($0 != "---OUTPUT---") {print; getline}} END {}' "$TMPFILE" > "io/$FULL_PROBLEM_ID.in" + awk '/^---OUTPUT---$/ {getline; while ($0 != "---END---") {print; getline}} END {}' "$TMPFILE" > "io/$FULL_PROBLEM_ID.expected" + echo "Scraped problem $FULL_PROBLEM_ID to io/$FULL_PROBLEM_ID.in and io/$FULL_PROBLEM_ID.expected" + else + echo "Failed to scrape problem $FULL_PROBLEM_ID" + echo "You can manually add test cases to io/$FULL_PROBLEM_ID.in and io/$FULL_PROBLEM_ID.expected" + cat "$TMPFILE" + rm "$TMPFILE" + exit + fi + ;; + *) + echo "Unknown contest type: $CONTEST" + echo "Available contests: cses, atcoder, codeforces" + rm "$TMPFILE" + exit + ;; +esac + +rm "$TMPFILE" diff --git a/templates/scripts/utils.sh b/templates/scripts/utils.sh new file mode 100644 index 0000000..b804089 --- /dev/null +++ b/templates/scripts/utils.sh @@ -0,0 +1,73 @@ +#!/bin/sh + +execute_binary() { + binary="$1" + input="$2" + output="$3" + is_debug="$4" + + start=$(date '+%s.%N') + if [ -n "$is_debug" ]; then + asan="$(ldconfig -p | grep libasan.so | head -n1 | awk '{print $4}')" + LD_PRELOAD="$asan" timeout 2s ./"$binary" <"$input" >"$output" 2>&1 + else + timeout 2s ./"$binary" <"$input" >"$output" 2>&1 + fi + CODE=$? + end=$(date '+%s.%N') + truncate -s "$(head -n 1000 "$output" | wc -c)" "$output" + + if [ $CODE -ge 124 ]; then + MSG='' + case $CODE in + 124) MSG='TIMEOUT' ;; + 128) MSG='SIGILL' ;; + 130) MSG='SIGABRT' ;; + 131) MSG='SIGBUS' ;; + 136) MSG='SIGFPE' ;; + 135) MSG='SIGSEGV' ;; + 137) MSG='SIGPIPE' ;; + 139) MSG='SIGTERM' ;; + esac + [ $CODE -ne 124 ] && sed -i '$d' "$output" + test -n "$MSG" && printf '\n[code]: %s (%s)' "$CODE" "$MSG" >>"$output" + else + printf '\n[code]: %s' "$CODE" >>"$output" + fi + + printf '\n[time]: %s ms' "$(awk "BEGIN {print ($end - $start) * 1000}")" >>$output + test -n "$is_debug" && is_debug_string=true || is_debug_string=false + printf '\n[debug]: %s' "$is_debug_string" >>$output + + expected_file="${output%.out}.expected" + if [ -f "$expected_file" ] && [ $CODE -eq 0 ]; then + awk '/^\[[^]]*\]:/ {exit} {print}' "$output" > /tmp/program_output + if cmp -s /tmp/program_output "$expected_file"; then + printf '\n[matches]: true' >>"$output" + else + printf '\n[matches]: false' >>"$output" + fi + rm -f /tmp/program_output + fi + + return $CODE +} + +compile_source() { + src="$1" + bin="$2" + output="$3" + flags="$4" + + test -f "$bin" && rm "$bin" || true + g++ @compile_flags.txt $flags "$src" -o "$bin" 2>"$output" + CODE=$? + + if [ $CODE -gt 0 ]; then + printf '\n[code]: %s' "$CODE" >>"$output" + return $CODE + else + echo '' >"$output" + return 0 + fi +} diff --git a/templates/snippets/template.cc b/templates/snippets/template.cc new file mode 100644 index 0000000..a1a79b3 --- /dev/null +++ b/templates/snippets/template.cc @@ -0,0 +1,9 @@ +#include +using namespace std; + +int main() { + ios_base::sync_with_stdio(false); + cin.tie(nullptr); + + return 0; +} diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..c6a66b0 --- /dev/null +++ b/uv.lock @@ -0,0 +1,181 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "beautifulsoup4" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "cloudscraper" +version = "1.2.71" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, + { name = "requests" }, + { name = "requests-toolbelt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/25/6d0481860583f44953bd791de0b7c4f6d7ead7223f8a17e776247b34a5b4/cloudscraper-1.2.71.tar.gz", hash = "sha256:429c6e8aa6916d5bad5c8a5eac50f3ea53c9ac22616f6cb21b18dcc71517d0d3", size = 93261, upload-time = "2023-04-25T23:20:19.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/97/fc88803a451029688dffd7eb446dc1b529657577aec13aceff1cc9628c5d/cloudscraper-1.2.71-py2.py3-none-any.whl", hash = "sha256:76f50ca529ed2279e220837befdec892626f9511708e200d48d5bb76ded679b0", size = 99652, upload-time = "2023-04-25T23:20:15.974Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "scrapers" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "cloudscraper" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.13.5" }, + { name = "cloudscraper", specifier = ">=1.2.71" }, + { name = "requests", specifier = ">=2.32.5" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] diff --git a/vim.toml b/vim.toml new file mode 100644 index 0000000..eac769a --- /dev/null +++ b/vim.toml @@ -0,0 +1,12 @@ +[selene] +base = "lua52" +name = "vim" + +[vim] +any = true + +[map] +any = true + +[bmap] +any = true