diff options
author | Karl Berry <karl@freefriends.org> | 2023-06-12 20:16:13 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2023-06-12 20:16:13 +0000 |
commit | 37a92634199a5d311afccd7d1dcdcb5160df452c (patch) | |
tree | fcf20fd813645e7fadb53eacf166cf4e572cb9df /Master/texmf-dist/tex/lualatex/luahttp | |
parent | 08572b84d6eb93111b79c28ca9b673ba284a173c (diff) |
luahttp (12jun23)
git-svn-id: svn://tug.org/texlive/trunk@67348 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/tex/lualatex/luahttp')
3 files changed, 568 insertions, 0 deletions
diff --git a/Master/texmf-dist/tex/lualatex/luahttp/luahttp-display.lua b/Master/texmf-dist/tex/lualatex/luahttp/luahttp-display.lua new file mode 100644 index 00000000000..1a41cfb22ca --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luahttp/luahttp-display.lua @@ -0,0 +1,278 @@ +--[[ +-- This module is part of the LuaHTTP package +-- The purpose of this module is to correctly display the data reveived from the fetch module. +]] + +local moduleName = display +local M = {} + +---------- Dependencies ------------------------ +local fetch = require("luahttp-fetch") + +---------- Local variables --------------------- +local tmp_image_counter = 0 -- Counter for image names + +---------- Helper functions -------------------- + +--- Displays an image using LuaTeX img.write function. +-- The image has to be saved first in order to be written to the PDF-Document using LuaTeX. +-- @param data image data +-- @param width optional width in cm +-- @param height optional height in cm +-- @see search_and_escape +local function display_image(data, width, height) + local tmp_image_name = '/tmp/tmp_image' -- filename of image saved temporarly + tmp_image_name = tmp_image_name .. tmp_image_counter + + local width = width or nil + local height = height or nil + local f = assert(io.open(tmp_image_name, 'wb')) + f:write(data) + f:close() + + -- LuaTeX does not provide built-in image scaling functions + local image = img.new({filename = tmp_image_name, width = width, height = height}) + if image then + img.write(image) + tmp_image_counter = tmp_image_counter + 1 + end +end + +--- Prompts the user to display an image. +-- If an image-URL is detected the user is asked to display the image or the plain URL. +-- @see is_image_url +local function prompt_user() + while true do + print("Do you want to display the image? (y/n)") + local answer = io.read() + + if answer == 'y' then + return true + elseif answer == 'n' then + return false + else + print("Invalid answer. Please enter 'y' or 'n'.") + end + end +end + +--- Searches the given URL for image extensions. +-- @param url some URL +-- @return true if an image extension was found, false otherwise +-- @see search_and_escape +local function is_image_url(url) + local image_extensions = { "jpg", "jpeg", "png", "gif" } + for _, ext in ipairs(image_extensions) do + if string.match(url, "%." .. ext) then + return true + end + end + return false +end + +--- Searches the given value for special characters that cause problems in LaTeX-Documents. +-- @param value single value of a table +-- @return if no special characters where found the value is retured unchanged, +-- if special characters where found the escaped value is returned, +-- if an image-URL is detected and the user chooses to display that image nil is returned +-- @see is_image_url, prompt_user +local function search_and_escape(value) + local value = tostring(value) + if string.find(value, "^http") then + if is_image_url(value) then + print("\nLooks like this URL leads to an image: " .. value) + if prompt_user() then + local body = fetch.image(value) + display_image(body, "5cm") + return nil + else + print("\nEscaping URL: " .. value) + value = [[\url{]] .. value .. [[}]] + end + else + print("\nEscaping URL: " .. value) + value = [[\url{]] .. value .. [[}]] + end + else + local latex_special_chars = '([%%$%{%}&%#_%^%~])' + value = value:gsub(latex_special_chars, "\\%1") + end + return value +end + +--- Prints a table to stdout. +-- @param t tagle to print +-- @param indent optional string used for indents +local function print_table(t, indent) + indent = indent or "" + for k, v in pairs(t) do + if type(v) == "table" then + print(indent .. k .. ":") + print_table(v, indent .. " ") + else + print(indent .. k .. ": " .. tostring(v)) + end + end +end + +--- Converts a table to text which can be written to the PDF-Document +-- The values of the table are first searched for special characters. +-- @param tbl table to be converted +-- @return text +-- @see search_and_escape +local function table_to_text(tbl) + local results = {} + for k, v in pairs(tbl) do + if type(v) == "table" then + table.insert(results, table_to_text(v)) + else + v = search_and_escape(v) + if v then + table.insert(results, v .. " \\\\ ") + end + end + end + return table.concat(results, " \\ ") +end + +--- Check if a table contains a certain value. +-- @param table input table +-- @param target_value value to be searched +-- @return true if value was found, false otherwise +local function table_contains(table, target_value) + for _, value in pairs(table) do + if type(value) == "table" then + table_contains(value, target_value) + elseif value == target_value then + return true + end + end + return false +end + +--- Filter out table entries that are not in the provided target keys. +-- @param input_table +-- @param target_keys array of target keys +-- @param results used for recursion +-- @return return a new table containing only the target keys and their values +local function filter_table(input_table, target_keys, results) + local results = results or {} + + for _, target_key in ipairs(target_keys) do + for key, value in pairs(input_table) do + if type(value) == "table" then + filter_table(value, target_keys, results) + elseif tostring(key) == target_key then + if not table_contains(results, value) then + table.insert(results, value) + break + end + end + end + end + return results +end + +--- Converts a string containing a comma seperated list of elements to an array (ipairs). +-- @param str input string +-- @return table containing the elements as values +local function string_to_ipairs(str) + local t = {} + for value in string.gmatch(str, "([^,]+)") do + table.insert(t, value) + end + return t +end + +---------- Module functions -------------------- + +--- Reads the contents of a JSON-file, filters the response and prints the result to the PDF-Document. +-- @param json_file_path path to the JSON-file +-- @param keys optional keys to filter out the relevant values from the response +function M.json_using_file(json_file_path, keys) + local data = fetch.json_using_file(json_file_path) + print_table(data) + if keys then + local keys = string_to_ipairs(tostring(keys)) + local values = filter_table(data, keys) + tex.sprint(table_to_text(values)) + else + tex.sprint(table_to_text(data)) + end +end + +--- Prints the response filtered by the keys to the PDF-Document. +-- @param url URL of the API +-- @param keys optional keys to filter out the relevant values from the response +function M.json(url, keys) + local data = fetch.json(tostring(url)) + print_table(data) + if keys then + local keys = string_to_ipairs(tostring(keys)) + local values = filter_table(data, keys) + tex.sprint(table_to_text(values)) + else + tex.sprint(table_to_text(data)) + end +end + +--- Print an image to the PDF-Document. +-- @param url URL of the image +-- @param width optional width in cm +-- @param height optional height in cm +function M.image(url, width, height) + local data = fetch.image(tostring(url)) + display_image(data, width, height) +end + +--- Print values from an rss-feed to the PDF-Document. +-- @param url URL of the feed +-- @param limit limits the amount of entries that get printed to the PDF-Document +-- @param feed_info_keys keys used to filter the feed information +-- @param entry_keys keys used to filter the feed entries +function M.rss(url, limit, feed_info_keys, entry_keys) + local data = fetch.rss(tostring(url)) + + if feed_info_keys then + local feed_info_keys = string_to_ipairs(tostring(feed_info_keys)) + local feed = data.feed + local feed_info_filtered = filter_table(feed, feed_info_keys) + + tex.sprint(table_to_text(feed_info_filtered)) + end + + local entries = {} + + for i = 1, limit do + if data.entries[i] then + table.insert(entries, data.entries[i]) + end + end + + if entry_keys then + local entry_keys = string_to_ipairs(tostring(entry_keys)) + local entries_filtered = filter_table(entries, entry_keys) + + print_table(entries_filtered) + tex.sprint(table_to_text(entries_filtered)) + else + tex.sprint(table_to_text(entries)) + end +end + +--- Print the reponse from a request using query parameters to the PDF-Document. +-- @param url URL of the API +-- @param keys keys to filter out the relevant values +-- @param ... multiple optional query parameters used in the request +function M.json_using_query(url, keys, ...) + local query_parameters = { ... } + local data = fetch.json_using_query(url, query_parameters) + + print_table(data) + + local keys = string_to_ipairs(tostring(keys)) + local values = filter_table(data, keys) + tex.sprint(table_to_text(values)) +end + +return M diff --git a/Master/texmf-dist/tex/lualatex/luahttp/luahttp-fetch.lua b/Master/texmf-dist/tex/lualatex/luahttp/luahttp-fetch.lua new file mode 100644 index 00000000000..c39997173d3 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luahttp/luahttp-fetch.lua @@ -0,0 +1,199 @@ +--[[ +-- This module is part of the LuaHTTP package +-- The purpose of this module is to make HTTP requests and return the response. +-- +-- Dependencies: +-- dkjson +-- luasec +-- ltn12 +-- feedparser +]] + +local moduleName = fetch +local M = {} + +---------- Dependencies ------------------------ +local http = require("socket.http") +local urlsocket = require("socket.url") +local https = require("ssl.https") +local dkjson = require("dkjson") +local ltn12 = require("ltn12") +local feedparser = require("feedparser") + +---------- Local variables --------------------- + +---------- Helper functions -------------------- + +--- Makes an HTTP request using the provided request parameter. +-- @param request table containing the request parameters +-- @return the response as a table +local function http_request(request) + local url = request.url + print("\nConnecting to " .. url) + + -- Detect HTTPS + local client = http + if url:lower():find("^https://") then + client = https + end + + -- Save optional body + local body = request.body + + -- Prepare request + local response = {} + local request = { + method = request.method or "GET", + url = url, + headers = request.headers or nil, + redirect = request.redirect or false, + sink = ltn12.sink.table(response) + } + + -- Send optional body + if body then + if type(body) == "table" then + body = dkjson.encode(body) + end + request.source = ltn12.source.string(body) + request.headers["Content-Length"] = #body + end + + -- Make the request + local response_status, response_code, response_header, response_message = client.request(request) + + local message = response_message or "(No response message recieved)" + + if response_status == nil then + error("\n!!! Error connecting to " .. url .. "\nResponse: " .. response_code .. "\nMessage: " .. message) + end + + -- Check for redirects and return body + if response_code == 301 or response_code == 302 or response_code == 303 then + print("\nResponse " .. message) + local redirect_url = response_header["location"] + if redirect_url == url then + error("\n!!! Error connecting to " .. url .. " results in a redirection loop") + else + print("\n!! Warning: redirecting to " .. redirect_url) + request.url = redirect_url + return http_request(request) + end + elseif response_code == 200 then + print("\nResponse " .. message) + if response == null or not next(response) then + error("\n!!! Error empty response") + end + return response + else + error("\n!!! Error connecting to " .. url .. "\nResponse: " .. response_code .. "\nMessage: " .. message) + end +end + +--- Parse the given JSON-file. +-- @param file_path path to JSON-file +-- @return table containing the JSON data +local function parse_json_file(file_path) + local file = io.open(file_path, "r") + local content = file:read("*all") + file:close() + return dkjson.decode(content) +end + +--- Split a given string on the first occurence of a given character. +-- @param str string containing the given character +-- @param char target character at which the string gets split +-- @return table containing the first part of the string as the key and the second part as the value +local function split_first(str, char) + local result = {} + local pos = str:find(char) + local key = str:sub(1, pos - 1) + local value = str:sub(pos + 1) + result[key] = value + return result +end + +---------- Module functions -------------------- + +--- Make a GET request using the provided URL +-- @param url target URL +-- @return table containg the response +function M.json(url) + local request = { + method = "GET", + url = url, + headers = { + ["Accept"] = "application/json" + }, + } + local response = http_request(request) + return dkjson.decode(table.concat(response)) +end + +--- Make a request using the provided JSON-file +-- @param json_file_path path to JSON-file +-- @return table containg the response +function M.json_using_file(json_file_path) + print("\nUsing file " .. json_file_path) + local request = parse_json_file(json_file_path) + local response = http_request(request) + return dkjson.decode(table.concat(response)) +end + +--- Make a GET request using the provided URL +-- @param url target URL +-- @return table containg the response +function M.rss(url) + local request = { + method = "GET", + url = url, + headers = { + ["Accept"] = "application/rss+xml" + }, + } + local response = http_request(request) + return feedparser.parse(table.concat(response)) +end + +--- Fetch image data using the provided URL +-- @param url target URL leading to an image +-- @return image data +function M.image(url) + local request = { + method = "GET", + url = url + } + local response = http_request(request) + return table.concat(response) +end + +--- Make a POST request using the provided URL and query parameters +-- @param url target URL +-- @param query_parameters parameters sent in the URL +-- @return table containg the response +function M.json_using_query(url, query_parameters) + local url = url + for _, value in ipairs(query_parameters) do + local params = split_first(value, "=") + for k, v in pairs(params) do + v = string.gsub(v, "\n", "") + url = url .. k .. "=" .. urlsocket.escape(v) + end + end + + print("\nURL: " .. url) + + local request = { + method = "POST", + url = url, + headers = { + ["Accept"] = "application/json", + ["Content-Type"] = "application/x-www-form-urlencoded"; + }, + redirect = false + } + local response = http_request(request) + return dkjson.decode(table.concat(response)) +end + +return M diff --git a/Master/texmf-dist/tex/lualatex/luahttp/luahttp.sty b/Master/texmf-dist/tex/lualatex/luahttp/luahttp.sty new file mode 100644 index 00000000000..627687f3cb3 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luahttp/luahttp.sty @@ -0,0 +1,91 @@ +%% luahttp.sty +%% Copyright 2023 Johannes Casaburi +% +% This work may be distributed and/or modified under the +% conditions of the LaTeX Project Public License, either version 1.3 +% of this license or (at your option) any later version. +% The latest version of this license is in +% https://www.latex-project.org/lppl.txt +% and version 1.3c or later is part of all distributions of LaTeX +% version 2008 or later. +% +% This work has the LPPL maintenance status `maintained'. +% +% The Current Maintainer of this work is Johannes Casaburi (johannes.casaburi@protonmail.com). +% +% This work consists of the files luahttp.sty, display.lua and fetch.lua. + +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{luahttp}[LuaHTTP Package, Version 1.0.1] + +\RequirePackage{ifluatex} +\RequirePackage{url} +\RequirePackage{xparse} + +\ifluatex + \RequirePackage{luapackageloader} + \directlua{ + version = 5.3 + package.path = 'lua_modules/share/lua/' .. version .. '/?.lua;lua_modules/share/lua/' .. version .. '/?/init.lua;' .. package.path + package.cpath = 'lua_modules/lib/lua/' .. version .. '/?.so;' .. package.cpath + display = require("luahttp-display") + } + + % \fetchJson{URL}[optional: "key1,key2,.."] + \NewDocumentCommand{\fetchJson}{m o}{ + \IfNoValueTF{#2} + {\directlua{display.json("\luaescapestring{#1}")}} % without arg 2 + {\directlua{display.json("\luaescapestring{#1}", "\luaescapestring{#2}")}} + } + + % \fetchJsonUsingFile{JSON file}[optional: "key1,key2,.."] + \NewDocumentCommand{\fetchJsonUsingFile}{m o}{ + \IfNoValueTF{#2} + {\directlua{display.json_using_file("\luaescapestring{#1}")}} % without arg 2 + {\directlua{display.json_using_file("\luaescapestring{#1}", "\luaescapestring{#2}")}} + } + + % \fetchJsonUsingQuery{URL}{"key1,key2,.."} [optional: "queryparameter1=value1"] .. [optional: "queryparameter5=value5"] + \NewDocumentCommand{\fetchJsonUsingQuery}{m m o o o o o}{ + \IfNoValueTF{#7} + {\IfNoValueTF{#6} + {\IfNoValueTF{#5} + {\IfNoValueTF{#4} + {\IfNoValueTF{#3} + {\directlua{display.json_using_query("\luaescapestring{#1}", "\luaescapestring{#2}")}} + {\directlua{display.json_using_query("\luaescapestring{#1}", "\luaescapestring{#2}", "\luaescapestring{#3}")}} + } + {\directlua{display.json_using_query("\luaescapestring{#1}", "\luaescapestring{#2}", "\luaescapestring{#3}", "\luaescapestring{#4}")}} + } + {\directlua{display.json_using_query("\luaescapestring{#1}", "\luaescapestring{#2}", "\luaescapestring{#3}", "\luaescapestring{#4}", "\luaescapestring{#5}")}} + } + {\directlua{display.json_using_query("\luaescapestring{#1}", "\luaescapestring{#2}", "\luaescapestring{#3}", "\luaescapestring{#4}", "\luaescapestring{#5}", "\luaescapestring{#6}")}} + } + {\directlua{display.json_using_query("\luaescapestring{#1}", "\luaescapestring{#2}", "\luaescapestring{#3}", "\luaescapestring{#4}", "\luaescapestring{#5}", "\luaescapestring{#6}", "\luaescapestring{#7}")}} + } + + % \fetchRss{URL}{limit}[optional: "feedinfokey1,feedinfokey2,.."][optional: "entrykey1,entrykey2,.."] + \NewDocumentCommand{\fetchRss}{m m o o}{ + \IfNoValueTF{#4} + {\IfNoValueTF{#3} + {\directlua{display.rss("\luaescapestring{#1}", #2)}} + {\directlua{display.rss("\luaescapestring{#1}", #2, "\luaescapestring{#3}")}} + } % without arg 4 + {\directlua{display.rss("\luaescapestring{#1}", #2, "\luaescapestring{#3}", "\luaescapestring{#4}")} + } + } + + %\fetchImage{URL}[optional: width][optional: height] + \NewDocumentCommand{\fetchImage}{m o o}{ + \IfNoValueTF{#2} + {\directlua{display.image("\luaescapestring{#1}")}} % whithout arg 2 + {\IfNoValueTF{#3} + {\directlua{display.image("\luaescapestring{#1}", "\luaescapestring{#2}")}} % without arg 3 + {\directlua{display.image("\luaescapestring{#1}", "\luaescapestring{#2}", "\luaescapestring{#3}")}} + } + } + +\else + \PackageError{luatexhttp}{LuaTeX is required}\@ehd + \expandafter\endinput % abort early +\fi |