----------------------------------------------------------------------- -- ODT exporter in Lua for SciTE -- Version 0.9.2, 20070805 -- -- Copyright 2004-2007 by Kein-Hong Man -- All Rights Reserved -- -- Permission to use, copy, modify, and distribute this software and -- its documentation for any purpose and without fee is hereby granted, -- provided that the above copyright notice appear in all copies and -- that both that copyright notice and this permission notice appear -- in supporting documentation. -- -- KEIN-HONG MAN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, -- INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN -- NO EVENT SHALL KEIN-HONG MAN BE LIABLE FOR ANY SPECIAL, INDIRECT OR -- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION -- WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -- ----------------------------------------------------------------------- -- USAGE -- -- * Tested on OpenOffice.org 2.0beta (Win32), will not load on OOo 1.x -- * Remember to update odt_preset.GENERATOR when version number changes -- * Control characters are shown as numbers in << >> delimiters. -- * If non-automatic named styles is selected, you can find the SciTE -- styles and modify them using the OpenOffice.org Stylist (F11). -- * Uses Reuben Thomas' bitwise function library if available, otherwise -- it falls back to a slow 4-bit per round lookup xor function. The -- library is currently in Bruce Dodson's recent builds (20040921). -- -- * If your file uses UTF-8, then they are passed verbatim to OpenOffice, -- which should be able to display them without any trouble (make sure -- you have set Encoding to UTF-8 first, though.) -- * UCS-2 currently not handled, while 8-bit values >127 are simply -- converted to UTF-8 without any specific charset conversion. You might -- need to use a charset converter to do things correctly. -- * If the ODT refuses to open, it may be because the encoding of your -- source document is not UTF-8 compliant. -- -- * TODO eolfilled style property does not work (yet) -- * TODO reverse video control characters (but not sure how to -- slightly separate consecutive runs of control characters) ----------------------------------------------------------------------- -- OPTIONS -- -- * Paste them into your SciTEUser.properties file or equivalent in -- order to make adjustments to the output. The property values given -- are the script defaults. -- * In the "configurable stuff" section in this script (line > 120): -- (1) Table odt.font_substitute allows you to substitute fonts -- (2) Table odt.paper_substitute is a table of paper sizes -- -- * These are 0/1-style false/true properties: --[[ # if 1, explicitly specify whites (for opaque whites) export.odt.whitewhites=0 # if 1, convert tabs to spaces (width in "tabsize" property) export.odt.converttabs=1 # if 1, use document's own tab width to format output export.odt.doctabs=1 # if 1, output used styles only export.odt.styleused=1 # if 1, enable background colouring export.odt.background=1 # if 0, disables font size, bold, italics, underline styles # to disable fonts, use export.odt.monofont export.odt.wysiwyg=1 # if 1, enable colours in exported documents export.odt.colour=1 # if 0, make styles available in Stylist for modifying export.odt.autostyle=0 # if 0, don't apply a text style to whitespace-only segments export.odt.spacestyled=0 --]] -- -- * These are properties that require values: --[[ # magnification (added to screen font size) export.odt.magnification=0 # if a font is specified, this overrides all other fonts, can be # used as a monospace font mode enabler export.odt.monofont=Courier New # size of tab stops, should have a unit suffix export.odt.tabstop=0.5in # portrait or landscape page orientation export.odt.orientation=portrait # type of paper or a "width, height" pair with proper units export.odt.pagesize=a4 # margins: "top, bottom, left, right", with proper units export.odt.margins=1in,1in,1in,1in --]] -- ------------------------------------------------------------------------ -- STRUCTURE -- -- * Section 1 is configurable stuff -- * Section 2 is for zero-compression deflate/zip -- * Section 3 is for ODT output snippets -- * Section 4 is the exporter proper ------------------------------------------------------------------------ -- if set, writes out a single XML file consisting of concatenated texts -- instead of a zero-compression zip file; easier to open and peek into --[[ local DEBUG = true --]] local math = math local string = string local table = table ----------------------------------------------------------------------- -- a simple check to alert of namespace collision, but allows different -- files to define their own functions in the exporters table ----------------------------------------------------------------------- if exporters then if exporters.SaveToODT then error("SciTE_ExporterODT: exporters.SaveToODT already defined") end -- set pass-through if charset translation not initialized if not exportutil.CharsetFromSet then exportutil.CharsetFromSet = function(c) return c end end else exporters = {} -- create table end ----------------------------------------------------------------------- -- configurable stuff ----------------------------------------------------------------------- local odt = {} local odt_preset = {} -- this is the identity of the creator odt_preset.GENERATOR = "SciTE Lua ODT Exporter 0.9.1" -- page settings defaults (please override using SciTE properties) odt_preset.TABSIZE = 4 odt_preset.TABSTOP = "0.5in" odt_preset.PAPER = "a4" -- case sensitive odt_preset.ORIENTATION = "portrait" odt_preset.MARGIN = "1in,1in,1in,1in" ----------------------------------------------------------------------- -- font substitution table ----------------------------------------------------------------------- odt.font_substitute = { --["Verdana"] = "Arial", -- example: switch all Verdana to Arial --[""] = "", } ----------------------------------------------------------------------- -- paper name substitution table (still based on SXW list) -- * some stuff on pages can be found in the following files: -- svx/inc/paperinf.hxx -- svx/source/dialog/{page.h|page.src|paperinf.cxx} ----------------------------------------------------------------------- odt.paper_substitute = { -- these were extracted from OpenOffice.org writer sample documents -- envelope sizes will probably not be very useful... ["a3"] = "11.6925in,16.5354in", ["a4"] = "8.2673in,11.6925in", ["a5"] = "5.8264in,8.2673in", ["letter"] = "8.5in,11in", ["legal"] = "8.5in,14.002in", ["tabloid"] = "11.0071in,16.9791in", ["b4 (iso)"] = "9.8425in,13.8972in", ["b5 (iso)"] = "6.9283in,9.8425in", ["b6 (iso)"] = "4.9209in,6.9283in", ["b4 (jis)"] = "10.1181in,14.3307in", ["b5 (jis)"] = "7.1654in,10.1181in", ["b6 (jis)"] = "5.039in,7.1654in", ["c4"] = "9.0161in,12.7555in", ["c5"] = "6.378in,9.0161in", ["c6"] = "4.4882in,6.378in", ["c65"] = "4.4882in,8.9374in", --[""] = "", } ------------------------------------------------------------------------ -- Declarations for pieces of an ODT document, strings and functions -- * these were extracted from a OpenOffice 2.0 beta Win32 sample doc -- * you can also refer to the OASIS Open Office Specification 1.0 as -- well, PDF is at: http://www.oasis-open.org/committees/office/ ------------------------------------------------------------------------ ------------------------------------------------------------------------ -- must be the first entry for unix magic number mechanism, see specs -- "mimetype" should be at file pos +30 -- actual mimetype should be at file pos +38 ------------------------------------------------------------------------ odt_preset.mimetype = 'application/vnd.oasis.opendocument.text' -- mimetype ------------------------------------------------------------------------ -- settings.xml contents -- (xml) -- (settings_xml) ------------------------------------------------------------------------ odt_preset.settings_xml = [[ ]] ------------------------------------------------------------------------ -- Manifest file is currently hard-coded. -- manifest.xml contents -- (xml) -- (manifest_xml) depends on (mimetype) ------------------------------------------------------------------------ odt_preset.manifest_xml = [[ ]] ------------------------------------------------------------------------ -- meta.xml contents -- (xml) -- (MetaHeader) -- odt_make_meta(name, value) -- (MetaFooter) ------------------------------------------------------------------------ odt_preset.MetaHeader = [[ ]] odt_preset.MetaFooter = [[ ]] ----------------------------------------------------------------------- -- styles.xml contents -- (xml) -- (StyleHeader) -- -- -- -- -- (StyleFooter) ----------------------------------------------------------------------- odt_preset.StylesHeader = [[ ]] odt_preset.StylesFooter = [[ ]] -- for reference only, unused, see StyleParagraphBeg, StyleParagraphEnd odt_preset.StyleDefaultParagraph = [[ ]] odt_preset.StyleParagraphBeg = [[ ]] odt_preset.StyleDefaultStandard = [[ ]] -- note the %% for percentage sign odt_preset.PageMaster = [[ ]] odt_preset.MasterStyles = [[ ]] ----------------------------------------------------------------------- -- content.xml contents -- (xml) -- (ContentHeader) -- -- -- -- (ContentBodyPrelude) -- -- (ContentFooter) ----------------------------------------------------------------------- odt_preset.ContentHeader = [[ ]] -- optional; in odt_preset.ContentBodyPrelude = [[ ]] odt_preset.ContentFooter = [[ ]] ------------------------------------------------------------------------ -- xml snippets ------------------------------------------------------------------------ odt_preset.StyleHeader = [[ ]] odt_preset.FontDecl = [[ ]] odt_preset.FontName = [[ style:font-name="%s" ]] odt_preset.FontSize = [[ fo:font-size="%spt" style:font-size-asian="%spt" style:font-size-complex="%spt" ]] odt_preset.FontFore = [[ fo:color="%s" ]] -- if under "paragraph"/paragraph-properties, this does eolfilled -- if under "text"/text-properties, this does background colour odt_preset.FontBack = [[ fo:background-color="%s" ]] -- normal or bold odt_preset.FontBold = [[ fo:font-weight="%s" style:font-weight-asian="%s" style:font-weight-complex="%s" ]] -- normal or italic odt_preset.FontItalics = [[ fo:font-style="%s" style:font-style-asian="%s" style:font-style-complex="%s" ]] -- normal or single odt_preset.FontUnderline = [[ style:text-underline-type="%s" style:text-underline-style="solid" style:text-underline-width="auto" style:text-underline-color="font-color" ]] ------------------------------------------------------------------------ -- font handling for -- style:font-family-generic and style:font-pitch does not seem to be -- critical for operation, unless perhaps if the font is missing... ------------------------------------------------------------------------ function odt.add_font(font, family, pitch) if odt.fonts == nil then odt.fonts = {} end local font_family = "" local font_pitch = "" -- substitute font if appropriate local subst = odt.font_substitute[font] if subst then font = subst else -- handle family and pitch if specified if family then font_family = string.format([[ style:font-family-generic="%s"\n]], family) end if pitch then font_pitch = string.format([[ style:font-pitch="%s"\n]], pitch) end end local font_decl = string.format(odt_preset.FontDecl, font, font, font_family, font_pitch) for i,v in ipairs(odt.fonts) do if v == font_decl then return font end end table.insert(odt.fonts, font_decl) return font end function odt.font_table() local snippet = "" for i,v in ipairs(odt.fonts) do snippet = snippet..v end return "\n"..snippet.. "\n" end ----------------------------------------------------------------------- -- UNUSED control character lookup (note array starts from index 1) -- for future use, if reverse-video display-style format is needed ----------------------------------------------------------------------- odt.ascii_lookup = { "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", -- ascii 0-7 "BS", "HT", "LF", "VT", "FF", "CR", "SO", "SI", -- ascii 8-15 "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", -- ascii 16-23 "CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US", -- ascii 24-31 } ------------------------------------------------------------------------ -- zero-compression zip writer from nullzip.lua ver 0.9.5, by KHMan ------------------------------------------------------------------------ -- Selected Notes: -- * Note: these notes are for the 4-bit per round lookup xor... -- * the output is limited to primitive header blocks (no zip64 records) -- * CRC is disappointingly slow, is there any other way to do it? -- * writes out data at about 100K/sec on an Athlon 2500+ -- (100 calls to nullzip_test() 4.5K sxw writing test in 10 sec) -- * updated here to Lua 5.1 code 20070805; performance untested ------------------------------------------------------------------------ -- auto-select proper functions to use based on availability of the -- bitwise function library; then checks function vars just to be sure local bxor, band, bshr if type(bit) == "table" then bxor = bit.bxor band = bit.band bshr = bit.rshift end if not bxor or not band or not bshr then bxor = nil end ------------------------------------------------------------------------ -- Pack values into little-endian string bytes ------------------------------------------------------------------------ local function u16(n) local c0 = n % 256; n = (n - c0) / 256 local c1 = n % 256 return string.char(c0, c1) end local function u32(n) local c0 = n % 256; n = (n - c0) / 256 local c1 = n % 256; n = (n - c1) / 256 local c2 = n % 256; n = (n - c2) / 256 local c3 = n % 256 return string.char(c0, c1, c2, c3) end ------------------------------------------------------------------------ -- Compute bitwise xor using a 4 bit-at-a-time table lookup. Is there -- any faster xor-calculating algorithm on vanilla Lua? Or a CRC -- algorithm that doesn't use (or use fewer) xor (or bitwise) ops? -- * strings and handling lookups in byte chunks is not faster -- * a 64K lookup table is 40% faster -- * a 64K string byte-lookup is 50% faster -- * building 64K tables is slow, but perhaps can load a binary file... ------------------------------------------------------------------------ local xor_lookup, xor_init, xor if not bxor then xor_lookup = {} function xor_init() local idx = 0 for x = 0, 15 do for y = 0, 15 do local bz = 1 local xx = x; local yy = y xor_lookup[idx] = 0 for z = 1, 4 do if xx % 2 ~= yy % 2 then xor_lookup[idx] = xor_lookup[idx] + bz end xx = math.floor(xx / 2); yy = math.floor(yy / 2) bz = bz * 2 end idx = idx + 1 end end end function xor(x, y, size) local z = 0 local nz = 1 size = size or 8 for n = 1, size do local nx = x % 16; x = (x - nx) / 16 local ny = y % 16; y = (y - ny) / 16 z = xor_lookup[nx * 16 + ny] * nz + z nz = nz * 16 end return z end xor_init() end--if not bxor ------------------------------------------------------------------------ -- nulldeflate: wraps no-compression header bytes around data, split into -- separate blocks if necessary -- * OpenOffice 1.1.2 on Win32 doesn't accept method 0 (store) so -- deflating seems to be a must ------------------------------------------------------------------------ local nulldeflate if bxor then nulldeflate = function(ibuf) local obuf = {} while #ibuf > 0 do local seg local block local ilen = #ibuf if ilen > 65535 then -- cleave a block block = string.sub(ibuf, 1, 65535) ibuf = string.sub(ibuf, 65536) else -- whole block block = ibuf ibuf = "" end if #ibuf > 0 then seg = "\0" -- header bits 000, padded else -- to nearest byte boundary seg = "\1" -- 001, last block end local olen = #block seg = seg..u16(olen) -- LEN olen = bxor(olen, 65535) -- 16-bit xor seg = seg..u16(olen) -- NLEN table.insert(obuf, seg) table.insert(obuf, block) -- uncompressed data end return table.concat(obuf) end else-- if not bxor nulldeflate = function(ibuf) local obuf = "" while #ibuf > 0 do local block local ilen = #ibuf if ilen > 65535 then -- cleave a block block = string.sub(ibuf, 1, 65535) ibuf = string.sub(ibuf, 65536) else -- whole block block = ibuf ibuf = "" end if #ibuf > 0 then obuf = obuf.."\0" -- header bits 000, padded else -- to nearest byte boundary obuf = obuf.."\1" -- 001, last block end local olen = #block obuf = obuf..u16(olen) -- LEN olen = xor(olen, 65535, 4) -- 16-bit xor obuf = obuf..u16(olen) -- NLEN obuf = obuf..block -- uncompressed data end return obuf end end--if bxor ------------------------------------------------------------------------ -- Straight adaptation of CRC32 code from RFC 1952 (from ISO 3309/ITU-T -- V.42). The crc should be initialized to zero. Pre- and post- -- conditioning (one's complement) is performed within -- nullzip_update_crc so it shouldn't be done by the caller. ------------------------------------------------------------------------ local nullzip_crc_table -- table of CRCs of all 8-bit messages ------------------------------------------------------------------------ -- nullzip_make_crc_table: make the table for a fast CRC. -- Can be replaced by the precomputed table instead. -- Usage: crc = nullzip_crc(data) ------------------------------------------------------------------------ local nullzip_make_crc_table if bxor then nullzip_make_crc_table = function() local CONST1 = 0xEDB88320 nullzip_crc_table = {} for n = 0, 255 do local c = n for k = 0, 7 do if bit.mod(c, 2) == 1 then -- c = 0xedb88320L ^ (c >> 1); c = bxor(CONST1, bshr(c, 1)) else c = bshr(c, 1) end end nullzip_crc_table[n] = c end end else--if not bxor nullzip_make_crc_table = function() local CONST1 = 0xEDB88320 nullzip_crc_table = {} for n = 0, 255 do local c = n for k = 0, 7 do if c % 2 == 1 then -- c = 0xedb88320L ^ (c >> 1); c = xor(CONST1, math.floor(c / 2)) else c = math.floor(c / 2) end end nullzip_crc_table[n] = c end end end--if bxor ------------------------------------------------------------------------ -- nullzip_update_crc: update a running crc with a specified buffer -- Usage: crc = nullzip_crc(original_crc, data) ------------------------------------------------------------------------ local nullzip_update_crc if bxor then nullzip_update_crc = function(original_crc, data) local CONST1 = 0xFFFFFFFF local c = bxor(original_crc, CONST1) if not nullzip_crc_table then nullzip_make_crc_table() end for n = 1, #data do -- c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8); c = bxor(nullzip_crc_table[band(bxor(c, string.byte(data, n)), 255)], bshr(c, 8)) end return bxor(c, CONST1) end else--if not bxor nullzip_update_crc = function(original_crc, data) local CONST1 = 0xFFFFFFFF local c = xor(original_crc, CONST1) if not nullzip_crc_table then nullzip_make_crc_table() end for n = 1, #data do -- c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8); c = xor(nullzip_crc_table[xor(c, string.byte(data, n), 2)], math.floor(c / 256)) end return xor(c, CONST1) end end--if bxor ------------------------------------------------------------------------ -- nullzip_crc: return the crc of a specified buffer, as a binary string -- Usage: crc = nullzip_crc(data) ------------------------------------------------------------------------ local function nullzip_crc(data) return u32(nullzip_update_crc(0, data)) end ------------------------------------------------------------------------ -- Returns MS-DOS formatted date and time in 4 bytes. -- Usage: time = nullzip_time() ------------------------------------------------------------------------ local function nullzip_time() dt = os.date("*t") dt.year = (dt. year - 1980) % 128 local dostime = dt.hour * 2048 -- 15-11 hour 0-23 + dt.min * 32 -- 10-5 minutes 0-59 + math.floor(dt.sec / 2) -- 4-0 secs in 2s units local dosdate = dt.year * 512 -- 15-9 year 0-119 (f. 1980) + dt.month * 32 -- 8-5 month 1-12 + dt.day -- 4-0 days 1-31 return u16(dostime)..u16(dosdate) end ------------------------------------------------------------------------ -- Current zip file data. ------------------------------------------------------------------------ local nullzip_data -- local headers and file data local nullzip_cdr -- central directory record local nullzip_entries -- entries in central directory local nullzip_offset -- output file pointer ------------------------------------------------------------------------ -- Clears the central directory record data prior to writing files. -- Usage: nullzip_init(); ------------------------------------------------------------------------ function nullzip_init() nullzip_data = {} nullzip_cdr = {} nullzip_entries = 0 nullzip_offset = 0 end ------------------------------------------------------------------------ -- Adds a directory. Directory should have a trailing forward slash. -- Usage: nullzip_add_dir(dirname) ------------------------------------------------------------------------ function nullzip_add_dir(dirname) local dirlength = #dirname local mtime = nullzip_time() ---------------------------------------------------------------- -- local file header ---------------------------------------------------------------- local ziplfh = u32(0x04034b50).. -- DD signature u16(20).. -- DW version needed to extract u16(0).. -- DW general purpose bit flag u16(0).. -- DW compression method (0=none,8=deflate) mtime.. -- DD last mod time & date u32(0).. -- DD crc-32 u32(0).. -- DD compressed size u32(0).. -- DD uncompressed size u16(dirlength).. -- DW file name length u16(0).. -- DW extra field length dirname -- DB name of directory if DEBUG then local debug_data = "\n" table.insert(nullzip_data, debug_data) else table.insert(nullzip_data, ziplfh) end ---------------------------------------------------------------- -- central directory file header ---------------------------------------------------------------- local zipcfh = u32(0x02014b50).. -- DD signature u16(20).. -- DW version made by u16(20).. -- DW version needed to extract u16(0).. -- DW general purpose bit flag u16(0).. -- DW compression method mtime.. -- DD last mod time & date u32(0).. -- DD crc-32 u32(0).. -- DD compressed size u32(0).. -- DD uncompressed size u16(dirlength).. -- DW file name length u16(0).. -- DW extra field length u16(0).. -- DW file comment length u16(0).. -- DW disk number start u16(0).. -- DW internal file attributes u32(16).. -- DD external file attributes u32(nullzip_offset).. -- DD relative offset of local header dirname -- DB name of directory table.insert(nullzip_cdr, zipcfh) nullzip_entries = nullzip_entries + 1 nullzip_offset = nullzip_offset + #ziplfh end ------------------------------------------------------------------------ -- Adds a file, given the filename (plus relative path if it exists), and -- the file data. -- Usage: nullzip_add_file(filename, filedata) ------------------------------------------------------------------------ function nullzip_add_file(filename, filedata) local compdata = nulldeflate(filedata) local namelength = #filename local mtime = nullzip_time() local crc = "" if not DEBUG then crc = nullzip_crc(filedata) end local csize = u32(#compdata) local usize = u32(#filedata) ---------------------------------------------------------------- -- local file header ---------------------------------------------------------------- local ziplfh = u32(0x04034b50).. -- DD signature u16(20).. -- DW version needed to extract u16(0).. -- DW general purpose bit flag u16(8).. -- DW compression method mtime.. -- DD last mod time & date crc.. -- DD crc-32 csize.. -- DD compressed size usize.. -- DD uncompressed size u16(namelength).. -- DW file name length u16(0).. -- DW extra field length filename -- DB name of file if DEBUG then local debug_data = "\n"..filedata table.insert(nullzip_data, debug_data) else table.insert(nullzip_data, ziplfh) table.insert(nullzip_data, compdata) end ---------------------------------------------------------------- -- central directory file header ---------------------------------------------------------------- local zipcfh = u32(0x02014b50).. -- DD signature u16(20).. -- DW version made by u16(20).. -- DW version needed to extract u16(0).. -- DW general purpose bit flag u16(8).. -- DW compression method mtime.. -- DD last mod time & date crc.. -- DD crc-32 csize.. -- DD compressed size usize.. -- DD uncompressed size u16(namelength).. -- DW file name length u16(0).. -- DW extra field length u16(0).. -- DW file comment length u16(0).. -- DW disk number start u16(0).. -- DW internal file attributes u32(32).. -- DD external file attributes u32(nullzip_offset).. -- DD relative offset of local header filename -- DB name of file table.insert(nullzip_cdr, zipcfh) nullzip_entries = nullzip_entries + 1 nullzip_offset = nullzip_offset + #ziplfh + #compdata end ------------------------------------------------------------------------ -- Returns the whole data set ready to be written to disk. -- Usage: filedata = nullzip_filedata() ------------------------------------------------------------------------ function nullzip_filedata() ---------------------------------------------------------------- -- central directory record ---------------------------------------------------------------- local cdr = table.concat(nullzip_cdr) local centraldir = u32(0x06054b50).. -- DD central dir signature u16(0).. -- DW number of this disk u16(0).. -- DW disk with cen dir start u16(nullzip_entries).. -- DW entries in cen dir u16(nullzip_entries).. -- DW total entries u32(#cdr).. -- DD size of central directory u32(nullzip_offset).. -- DD offset of cen dir start u16(0) -- DW .ZIP file comment length -- perform minimal concatenations if DEBUG then return table.concat(nullzip_data) end cdr = cdr..centraldir table.insert(nullzip_data, cdr) return table.concat(nullzip_data) end -- * nullzip.lua test functions removed * ----------------------------------------------------------------------- -- exporters:SaveToODT -- -- Exports the document in the current window to an ODT format file. -- ----------------------------------------------------------------------- function exporters:SaveToODT() --------------------------------------------------------------------- -- XML standard entities (always present; part of XML standard) --------------------------------------------------------------------- local entities = { ["&"] = "&", ["<"] = "<", [">"] = ">", ["'"] = "'", ["\""] = """, } --------------------------------------------------------------------- -- text selection --------------------------------------------------------------------- local selBeg, selEnd = exportutil.GetSelPos() editor:Colourise(0, -1) --------------------------------------------------------------------- -- UTF-8 encoding flag (UCS-2 currently not handled), boolean --------------------------------------------------------------------- local utf8 = editor.CodePage == SC_CP_UTF8 -- was scite.SendEditor(SCI_GETCODEPAGE) (only in SciTE post-1.61) --------------------------------------------------------------------- -- tab to space value --------------------------------------------------------------------- local tabSize = tonumber(props["tabsize"]) if not tabSize or tabSize <= 0 or tabSize >= 99 then tabSize = odt_preset.TABSIZE end if exportutil.propbool("export.odt.doctabs", 1) then tabSize = editor.TabWidth end --------------------------------------------------------------------- -- boolean flags --------------------------------------------------------------------- -- TODO opaque whites may depend on a global ODT property; to check local whiteWhites = exportutil.propbool("export.odt.whitewhites") local convertTabs = exportutil.propbool("export.odt.converttabs", 1) local stylesUsed = exportutil.propbool("export.odt.styleused", 1) local background = exportutil.propbool("export.odt.background", 1) local wysiwyg = exportutil.propbool("export.odt.wysiwyg", 1) local colour = exportutil.propbool("export.odt.colour", 1) local autoStyle = exportutil.propbool("export.odt.autostyle") local spaceStyled = exportutil.propbool("export.odt.spacestyled") --------------------------------------------------------------------- -- valued properties --------------------------------------------------------------------- -- returns comma-separated fields in a property value as a list local function parseProp(propValue) local propList = {} if not propValue or propValue == "" then return propList end for v in string.gfind(propValue, "([^,]+),?") do table.insert(propList, v) end return propList end -- magnification value to add to screen font size local magnification = tonumber(props["export.odt.magnification"]) or 0 -- font override property local monoFont = props["export.odt.monofont"] if not monoFont or monoFont == "" then monoFont = nil end -- tab stop setting local tabStop = props["export.odt.tabstop"] if not tabStop or tabStop == "" then tabStop = odt_preset.TABSTOP end -- page orientation local orientation = props["export.odt.orientation"] if not orientation or orientation == "" then orientation = odt_preset.ORIENTATION end -- paper size: paper name or width, height local paper = string.lower(props["export.odt.pagesize"]) local pageSize = odt.paper_substitute[paper] or paper if not pageSize or pageSize == "" then pageSize = parseProp(odt.paper_substitute[odt_preset.PAPER]) if string.lower(orientation) == "landscape" then pageSize[1], pageSize[2] = pageSize[2], pageSize[1] end else pageSize = parseProp(pageSize) end -- page margins: top, bottom, left, right pageMargins = parseProp(props["export.odt.margins"]) if #pageMargins ~= 4 then pageMargins = parseProp(odt_preset.MARGIN) end --------------------------------------------------------------------- -- dump all styles or only styles that are used --------------------------------------------------------------------- local styleIsUsed = {} if stylesUsed then for i = selBeg, selEnd - 1 do styleIsUsed[exportutil.StyleAt(i)] = true end else for i = 0, exportutil.STYLE_MAX do styleIsUsed[i] = true end end styleIsUsed[exportutil.STYLE_DEFAULT] = true --------------------------------------------------------------------- -- style handling --------------------------------------------------------------------- stylemgr:setlexer(editor.Lexer) -- helper functions local function IsSpecified(sd, field) return (string.find(sd.specified, field, 1, 1)) end local function IsOpaqueColour(c) if not whiteWhites and c == "#FFFFFF" then return false end return true end -- create style names, make them more verbose if non-automatic styles -- selected (that is, named styles that can be listed and modified) local stylePrefix local style = {}; local rstyle = {} local styleName = {} if autoStyle then stylePrefix = "S" else stylePrefix = (stylemgr:getlexerlanguage() or "txt").."_S" end -- these are present in the normal default styles odt.add_font("Times New Roman") odt.add_font("Arial Unicode MS") odt.add_font("Tahoma") -- build style specifications for i = 0, exportutil.STYLE_MAX do if styleIsUsed[i] then -- read and process style entry local sd = stylemgr:locate(i) styleName[i] = [[]] -- start of style specification local st = string.format(odt_preset.StyleHeader, stylePrefix..i) if sd.specified ~= "" then -- handle font naming and monofont override if IsSpecified(sd, "font") then if not monoFont then local actual_font = odt.add_font(sd.font) st = st..string.format(odt_preset.FontName, actual_font) elseif monoFont and i == exportutil.STYLE_DEFAULT then local actual_font = odt.add_font(monoFont) st = st..string.format(odt_preset.FontName, actual_font) end end -- handle font properties optionally if wysiwyg or i == exportutil.STYLE_DEFAULT then if IsSpecified(sd, "size") then sd.size = sd.size + magnification st = st..string.format(odt_preset.FontSize, sd.size, sd.size, sd.size) end end if wysiwyg then if IsSpecified(sd, "italics") then local v = sd.italics and "italic" or "normal" st = st..string.format(odt_preset.FontItalics, v, v, v) end if IsSpecified(sd, "bold") then local v = sd.bold and "bold" or "normal" st = st..string.format(odt_preset.FontBold, v, v, v) end if IsSpecified(sd, "underlined") then local v = sd.underlined and "single" or "none" if v ~= "none" then st = st..string.format(odt_preset.FontUnderline, v) end end end--if wysiwyg --if IsSpecified(sd, "eolfilled") and sd.eolfilled then -- TODO low priority; is this even possible? --end -- handle foreground and background optionally if colour then local c = stylemgr:hexstr(sd.fore) if IsSpecified(sd, "fore") and IsOpaqueColour(c) then st = st..string.format(odt_preset.FontFore, c) end c = stylemgr:hexstr(sd.back) if background and IsSpecified(sd, "back") and IsOpaqueColour(c) then st = st..string.format(odt_preset.FontBack, c) end end--if colour end -- end of style specification style[i] = st..odt_preset.StyleFooter end--if styleIsUsed end--for i --------------------------------------------------------------------- -- patch in default styles from processed version of style number 32 -- because all styles will inherit them from "Standard" --------------------------------------------------------------------- local _, _, prop_add = string.find(style[exportutil.STYLE_DEFAULT], "%s\n") odt_preset.StyleParagraph = string.format(odt_preset.StyleParagraphBeg, tabStop).. prop_add.. odt_preset.StyleParagraphEnd --------------------------------------------------------------------- -- main loop --------------------------------------------------------------------- local data = {}; local line = {} local seg = ""; local notwhitespace = false local column = 0; local spaces = 0 local i = selBeg local styleCurrent --------------------------------------------------------------------- -- write out representation of space characters --------------------------------------------------------------------- function flushSpaces() if spaces == 1 then -- at the start of a paragraph, whitespace zapped, I think if line[2] then seg = seg.." " else seg = seg..[[]] end --elseif spaces == 2 then -- doesn't work, must have text:c prop -- seg = seg.." " -- used only *within* a style else-- spaces > 2 then seg = seg..string.format([[]], spaces) end spaces = 0 end --------------------------------------------------------------------- -- apply style to a line segment --------------------------------------------------------------------- function styleSegment(segment) if notwhitespace or spaceStyled then -- table.concat is slightly slower --return table.concat{styleName[styleCurrent], segment, [[]]} return styleName[styleCurrent]..segment..[[]] end return segment end while i < selEnd do local ch = exportutil.CharAt(i) local styleNow = exportutil.StyleAt(i) ------------------------------------------------------------------- -- handle newlines ------------------------------------------------------------------- if ch == "\n" or ch == "\r" then if ch == "\r" and exportutil.CharAt(i + 1) == "\n" then i = i + 1 end if column == 0 then -- empty line optimization table.insert(data, "\n") else table.insert(line, styleSegment(seg)) table.insert(line, "\n") table.insert(data, table.concat(line)) end column = 0 line = {}; styleCurrent = nil seg = ""; notwhitespace = false ------------------------------------------------------------------- -- handle style change (always true at beginning of line) ------------------------------------------------------------------- elseif styleNow ~= styleCurrent then -- close previous text span if required if column > 0 then if spaces > 0 then flushSpaces() end table.insert(line, styleSegment(seg)) else -- start of line here line = {[[]]} end seg = ""; notwhitespace = false styleCurrent = styleNow end--style ------------------------------------------------------------------- -- output runs of space characters ------------------------------------------------------------------- if spaces > 0 then if ch == " " or (ch == "\t" and convertTabs) then -- more spaces to come, keep counting else flushSpaces() end end ------------------------------------------------------------------- -- handle spaces (actually handled above) ------------------------------------------------------------------- if ch == " " then spaces = spaces + 1 column = column + 1 ------------------------------------------------------------------- -- handle tabs ------------------------------------------------------------------- elseif ch == "\t" then if convertTabs then local ts = tabSize - column % tabSize spaces = spaces + ts column = column + ts else seg = seg.."" column = column + 1 end ------------------------------------------------------------------- -- handle control characters ------------------------------------------------------------------- else local c = string.byte(ch) notwhitespace = true if c < 32 then if ch ~= "\r" and ch ~= "\n" then -- ALTERNATIVES control char handling seems to use style 36 -- use odt.ascii_lookup[string.byte(ch) + 1] to get ASCII names -- with the method below, we can still see the value... local ctrl = "«"..c.."»" seg = seg..ctrl column = column + #ctrl end ----------------------------------------------------------------- -- handle characters with codes > 127 (conversion to UTF-8 with -- optional charset conversion) ----------------------------------------------------------------- elseif c > 127 then if not utf8 then c = exportutil.CharsetToUTF8(c) if c >= 2048 then ch = string.char(224 + math.floor(c / 4096), 128 + math.floor(c / 64) % 64, 128 + c % 64) elseif c >= 128 then ch = string.char(192 + math.floor(c / 64), 128 + c % 64) else ch = string.char(c) end end seg = seg..ch column = column + 1 ----------------------------------------------------------------- -- handle normal characters ----------------------------------------------------------------- else seg = seg..(entities[ch] or ch) column = column + 1 end end--if ch i = i + 1 end--while -- clean up if last line has no EOL if seg ~= "" or #line > 0 then if spaces > 0 then flushSpaces() end table.insert(line, styleSegment(seg).."\n") table.insert(data, table.concat(line)) else table.insert(data, "\n") end --------------------------------------------------------------------- -- open exported file for saving --------------------------------------------------------------------- local fileExt = "odt"; if DEBUG then fileExt = "xml" end local saveName = exportutil.exportfile(props["FileDir"], props["FileName"], fileExt) local fp = io.open(saveName, "wb") if not fp then error("exporters:SaveToODT: could not save file \""..saveName.."\"") end if exportutil.VERBOSE ~= 0 then _ALERT("\nExporting to ODT, filepath: "..saveName) end --------------------------------------------------------------------- -- build the files --------------------------------------------------------------------- -- simple stripped-down files first, then more complex ones nullzip_init() -- mimetype (first file, for unix magic number handling) nullzip_add_file("mimetype", odt_preset.mimetype) -- manifest directory nullzip_add_dir("META-INF/") -- manifest.xml nullzip_add_file("META-INF/manifest.xml", odt_preset.manifest_xml) -- settings.xml nullzip_add_file("settings.xml", odt_preset.settings_xml) --------------------------------------------------------------------- -- meta.xml --------------------------------------------------------------------- -- builds a document meta information entry local function odt_make_meta(name, value) return " "..value.."\n" end local meta_xml = { odt_preset.MetaHeader, odt_make_meta("generator", odt_preset.GENERATOR), odt_make_meta("initial-creator", odt_preset.GENERATOR), -- ISO 8601 date odt_make_meta("creation-date", os.date("%Y-%m-%dT%H:%M:%S")), odt_preset.MetaFooter, } nullzip_add_file("meta.xml", table.concat(meta_xml)) --------------------------------------------------------------------- -- writes out SciTE-specific style set --------------------------------------------------------------------- local function odt_styles() local sData = "" for _,sItem in pairs(style) do sData = sData..sItem end return sData end --------------------------------------------------------------------- -- styles.xml (see earlier comments for structural outline) --------------------------------------------------------------------- local font_decl = odt.font_table() -- also in content.xml local font_styles = "" if not autoStyle then font_styles = odt_styles() end local pageSettings = string.format(odt_preset.PageMaster, pageSize[1], pageSize[2], orientation, pageMargins[1], pageMargins[2], pageMargins[3], pageMargins[4]) local styles_xml = { odt_preset.StylesHeader, font_decl, "\n", odt_preset.StyleParagraph, odt_preset.StyleDefaultStandard, font_styles, "\n", "\n", pageSettings, "\n", odt_preset.MasterStyles, odt_preset.StylesFooter, } nullzip_add_file("styles.xml", table.concat(styles_xml)) --------------------------------------------------------------------- -- content.xml (see earlier comments for structural outline) --------------------------------------------------------------------- font_styles = "" if autoStyle then font_styles = odt_styles() end local content_xml = { odt_preset.ContentHeader, font_decl, "\n", font_styles, "\n", "\n\n", table.concat(data), "\n\n", odt_preset.ContentFooter, } nullzip_add_file("content.xml", table.concat(content_xml)) --------------------------------------------------------------------- -- write and close exported file --------------------------------------------------------------------- fp:write(nullzip_filedata()) fp:close() if exportutil.VERBOSE ~= 0 then exportutil.progress("... done.") end end -- end of script