# ACTIVESTATE TEAPOT-PKG BEGIN TM -*- tcl -*- # -- Tcl Module # @@ Meta Begin # Package rtflib 1.0 # Meta as::author {Joe English} # Meta as::build::date 2015-03-24 # Meta as::origin http://www.flightlab.com/~joe/cost/ratfink/ # Meta category PDF # Meta description RTF generation for Tcl # Meta license BSD # Meta platform tcl # Meta require {Tcl 8.4} # Meta subject rtf # Meta summary RTF generation for Tcl # @@ Meta End # ACTIVESTATE TEAPOT-PKG BEGIN REQUIREMENTS package require Tcl 8.4 # ACTIVESTATE TEAPOT-PKG END REQUIREMENTS # ACTIVESTATE TEAPOT-PKG BEGIN DECLARE package provide rtflib 1.0 # ACTIVESTATE TEAPOT-PKG END DECLARE # ACTIVESTATE TEAPOT-PKG END TM ############################################################# # RATFINK v1.0 # A library of RTF output utilities # $Id: rtflib.tcl,v 1.22 2002/05/15 01:26:50 joe Exp $ # Created 4 Dec 1995 / Last updated $Date: 2002/05/15 01:26:50 $ ############################################################# # # Copyright (C) 1996-2002 Joe English # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose and without fee is hereby granted. # # THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED # WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. # ############################################################# # RATFINK lives at ############################################################# # %%% TODO: Rework rtf_state(inXXX) checks -- this is getting # %%% too complicated to do "by hand" (topics, sections, paragraphs, # %%% tables, links; etc.). Try an FSM derived from RTF meta-DTD. # %%% TODO: allow true/false/on/off/etc for boolean attributes # %%% TODO: rtf::zapFunkyCharacters for docTitle, contents file output, &c # %%% TODO: make sure all topics in a browse sequence use same window type package provide rtflib 1.0 namespace eval rtf { } proc rtf::warning {text} { puts stderr "RTF warning: $text" } proc rtf::undefined {class value} { global RTFUndefined if {![info exists RTFUndefined($class.$value)]} { set RTFUndefined($class.$value) 1 rtf::warning "undefined $class '$value'" } } ### ### Character formatting attributes ### array set rtf::CharAttrs { Font {table rtf::FontTable "f"} FontSize {halfpts "fs"} Bold {boolean "b"} Italic {boolean "i"} Hidden {boolean "v"} AllCaps {boolean "caps"} SmallCaps {boolean "scaps"} StrikeThru {boolean "strike"} Underline {enum { {0 ulnone} {None ulnone} {1 ul} {Continuous ul} {Single ul} {Dot uld} {Double uldb} {Word ulw} }} Subscript {flag "sub"} Superscript {flag "super"} NoSuperSub {flag "nosupersub"} ShiftDown {halfpts "dn"} ShiftUp {halfpts "up"} FGColor {table rtf::ColorTable "cf"} BGColor {table rtf::ColorTable "cb"} LetterSpace {dimension "expndtw"} KernAbove {halfpts "kerning"} TextDirection {enum { {RTL "rtlch"} {LTR "ltrch"} }} Charset {table rtf::CharsetTable "cchs"} Language {table rtf::LanguageTable "lang"} } ### ### Paragraph formatting attributes ### array set rtf::ParaAttrs { TabStops {proc rtf::ExpandTabStops} FirstIndent {dimension "fi"} LeftIndent {dimension "li"} RightIndent {dimension "ri"} SpaceBefore {dimension "sb"} SpaceAfter {dimension "sa"} LineSpacing {dimension "sl"} KeepTogether {flag "keep"} KeepWithNext {flag "keepn"} PageBreakBefore {flag "pagebb"} Hyphenate {boolean "hyphpar"} Quadding {enum { {Left "ql"} {Right "qr"} {Justify "qj"} {Center "qc"} }} InnerBorders {flag "brdrbtw"} Box {proc rtf::ExpandRuleStyle "box"} TopBorder {proc rtf::ExpandRuleStyle "brdrt"} BottomBorder {proc rtf::ExpandRuleStyle "brdrb"} LeftBorder {proc rtf::ExpandRuleStyle "brdrl"} RightBorder {proc rtf::ExpandRuleStyle "brdrr"} OutsideBorder {proc rtf::ExpandRuleStyle "brdrbar"} } # All character attributes are also paragraph attributes: array set rtf::ParaAttrs [array get rtf::CharAttrs] ### ### Section formatting attributes ### array set rtf::SectAttrs { PageWidth {dimension "pgwsxn"} PageHeight {dimension "pghsxn"} LeftMargin {dimension "marglsxn"} RightMargin {dimension "margrsxn"} TopMargin {dimension "margtsxn"} BottomMargin {dimension "margbsxn"} GutterWidth {dimension "guttersxn"} Landscape {flag "lndscpsxn"} HasTitlePage {flag "titlepg"} HeaderPosition {dimension "headery"} FooterPosition {dimension "footery"} SectionBreak {enum { {None "sbknone"} {Page "sbkpage"} {EvenPage "sbkeven"} {OddPage "sbkodd"} }} VAlign {enum { {Top "vertalt"} {Bottom "vertalb"} {Middle "vertalc"} {Center "vertalc"} {Justify "vertalj"} }} FirstPageNumber {integer starts} RestartPageNumbers {enum { {0 "pgncont"} {1 "pgnrestart"} }} PageNumbering {enum { {Arabic "pgndec"} {Decimal "pgndec"} {UCRoman "pgnucrm"} {LCRoman "pgnlcrm"} {UCAlpha "pgnucltr"} {LCAlpha "pgnlcltr"} }} Columns {integer "cols"} ColumnSepSpace {dimension "colsx"} ColumnLine {flag "linebetcol"} ColumnDirection {enum { {RTL "rtlsect"} {LTR "ltrsect"} }} } ### ### Table attributes: ### # nb: CellPosition is really a dimension array set rtf::TableAttrs { CellPosition {integer "cellx"} RowGap {dimension "trgaph"} RowHeight {dimension "trrh"} HeadingRow {flag "trhdr"} Align {enum { {Left "trql"} {Right "trqr"} {Center "trqc"} }} CellBorderBottom {table rtf_RuleTable "clbrdrb"} CellBorderTop {table rtf_RuleTable "clbrdrt"} CellBorderLeft {table rtf_RuleTable "clbrdrl"} CellBorderRight {table rtf_RuleTable "clbrdrr"} } global rtf_tableSettings rtf_tableDefaults array set rtf_tableDefaults { width 6in numcols 1 numcells 1 rowgap 0pt rowsep - colsep - frame - toprule - botrule - rowalign Center } array set rtf_tableSettings [array get rtf_tableDefaults] ### ### Style attribute handling ### proc rtf::ExpandAttributes {tblName attlist} { upvar #0 $tblName table set result "" while {[llength $attlist] >= 2} { set param [lindex $attlist 0] set value [lindex $attlist 1] set attlist [lrange $attlist 2 end] if {![info exists table($param)]} { rtf::warning "No parameter $param in $tblName" continue } set key $table($param) switch [lindex $key 0] { unknown { rtf::warning "Don't grok parameter $param=$value in $tblName" continue } boolean { set csname [lindex $key 1] if {$value} { set cs "\\$csname" } else { set cs "\\${csname}0" } } flag { set csname [lindex $key 1] if {$value} { set cs "\\$csname" } else { rtf::warning "Parameter $param can only be turned on" } } integer { if {![regexp -- {^-?[0-9]+$} $value]} { rtf::warning "bad integer $value in parameter $param" continue } set csname [lindex $key 1] set cs "\\${csname}$value" } dimension { set csname [lindex $key 1] set cs "\\${csname}[rtf::cvTwips $value]" } halfpts { set csname [lindex $key 1] set cs "\\${csname}[rtf::cvHalfpts $value]" } enum { catch {unset cs} set enumnames {} foreach enumpair [lindex $key 1] { set enumname [lindex $enumpair 0] if {[string match $value $enumname]} { set cs "\\[lindex $enumpair 1]"; break; } lappend enumnames $enumname; } if {![info exists cs]} { rtf::warning \ "Unknown value $param $value: must be one of $enumnames" continue } } table { set subtblName [lindex $key 1] upvar #0 $subtblName subtable if {![info exists subtable($value)]} { rtf::warning "$param: no mapping for $value in $subtblName" continue } if {[llength $key] == 3} { set cs "\\[lindex $key 2]$subtable($value)" } else { set cs $subtable($value) } } proc { set procName [lindex $key 1] set cs [$procName $value] if {[llength $key] == 3} { set cs "\\[lindex $key 2]$cs" } } pseudo { # pseudo-argument; ignore set cs "" } default { rtf::warning "Bad parameter type [lindex $key 0] for $param" continue } } append result $cs } if {[llength $attlist] != 0} { rtf::warning "Leftover arguments: <$attlist>" } return $result } # Expand attributes from associative array. # 'paramlist' if present specifies the desired output order. # proc rtf::ExpandArray {tblName specsName {paramlist {}}} { upvar #0 $tblName table upvar 1 $specsName specs set speclist {} if {$paramlist == {}} { set paramlist [array names specs] } foreach param $paramlist { if {[info exists specs($param)]} { lappend speclist $param $specs($param) } } # error-check: foreach param [array names specs] { if {![info exists table($param)]} { rtf::warning "No parameter $param in $tblName" } } return [rtf::ExpandAttributes $tblName $speclist] } ### ### Stylesheet management ### global rtf_styleSheet; # key: $styleName,(DESC|NUM|DEF|TYPE|BASEDON) global rtf_styleNames; set rtf_styleNames {} global rtf_styleNum; set rtf_styleNum 1 proc rtf::paraStyle {name desc args} { rtf::defineStyle PARA $name $desc $args } proc rtf::charStyle {name desc args} { rtf::defineStyle CHAR $name $desc $args } proc rtf::sectStyle {name desc args} { rtf::defineStyle SECT $name $desc $args } proc rtf::defineStyle {type name desc arglist} { global rtf_styleNum rtf_styleSheet rtf_styleNames set rtf_styleSheet($name,NUM) $rtf_styleNum set rtf_styleSheet($name,DESC) $desc set rtf_styleSheet($name,TYPE) $type set styledef "" set rankGroup #NONE while {[string match "-*" [lindex $arglist 0]]} { set option [lindex $arglist 0] set value [lindex $arglist 1] set arglist [lrange $arglist 2 end] switch -- $option { -basedon { if {![string length $value]} { continue } if {![info exists rtf_styleSheet($value,DEF)]} { rtf::undefined style $value continue } if {$rtf_styleSheet($value,TYPE) != $type} { rtf::warning "$name based on style of different type" continue } set rtf_styleSheet($name,BASEDON) $value append styledef "$rtf_styleSheet($value,DEF)" } -rankgroup { variable rankGroups set rankGroup $value if {![info exists rankGroups($rankGroup.styles)]} { rtf::undefined rankGroup $rankGroup rtf::rankGroup $rankGroup } lappend rankGroups($rankGroup.styles) $name } default { rtf::warning "rtf::defineStyle -- bad option $option" } } } set attlist [lindex $arglist 0] switch $type { PARA { set styletable rtf::ParaAttrs } CHAR { set styletable rtf::CharAttrs } SECT { set styletable rtf::SectAttrs } } set stylespec [rtf::ExpandAttributes $styletable $attlist] append styledef $stylespec set rtf_styleSheet($name,DEF) $styledef if {[string compare $rankGroup #NONE]} { set rtf_styleSheet(${name}0,DEF) $styledef set rtf_styleSheet(${name}0,NUM) $rtf_styleNum } incr rtf_styleNum lappend rtf_styleNames $name return; } ### ### Ranked styles. ### proc rtf::rankGroup {name} { variable rankGroups set rankGroups($name.styles) [list] set rankGroups($name.level) 0 } proc rtf::rankedStyle {baseName rankLevel styleSpec} { global rtf_styleSheet rtf::defineStyle PARA "${baseName}${rankLevel}" \ "$rtf_styleSheet($baseName,DESC) level $rankLevel" \ [list -basedon $baseName $styleSpec] \ ; } proc rtf::rankLevel {group incr} { global rtf_styleSheet variable rankGroups set level [incr rankGroups($group.level) $incr] foreach baseStyle $rankGroups($group.styles) { set curStyle "${baseStyle}${level}" if {![info exists rtf_styleSheet($curStyle,NUM)]} { rtf::undefined rank $curStyle continue } set rtf_styleSheet($baseStyle,NUM) $rtf_styleSheet($curStyle,NUM) set rtf_styleSheet($baseStyle,DEF) $rtf_styleSheet($curStyle,DEF) } } ### ### Emit stylesheet. ### proc rtf::writeStyleSheet {} { global rtf_styleNames rtf_styleSheet rtf::bgroup rtf::write "\\stylesheet" foreach style $rtf_styleNames { rtf::bgroup switch $rtf_styleSheet($style,TYPE) { PARA { set styletype "\\s" } CHAR { set styletype "\\cs" } SECT { set styletype "\\ds" } } rtf::write "$styletype$rtf_styleSheet($style,NUM)" rtf::write "$rtf_styleSheet($style,DEF)" if {[info exists rtf_styleSheet($style,BASEDON)]} { set basedon $rtf_styleSheet($style,BASEDON) rtf::write "\\sbasedon$rtf_styleSheet($basedon,NUM)" } rtf::write " $rtf_styleSheet($style,DESC);" rtf::egroup } rtf::egroup } ### ### Dimensions ### global rtf_twipsMap array set rtf_twipsMap " twip 1 pt 20 halfpt 10 in [expr 72 * 20] pc [expr 12 * 20] pica [expr 12 * 20] cm 567 mm 56.7 " # %%% The conversion factor 56.7 for 'mm' was determined -- # %%% by experimentation -- so RATFINK's definition of A4 paper (210mm x 297mm) # %%% matches WFW95v7's definition (\paperw11907\paperh16840) # %%% after rounding. [expr (72 * 20) / 25.4] == 56.6929 # %%% is a *little* bit smaller than what Word appears to use. proc rtf::cvTwips {dim} { global rtf_twipsMap if {![regexp -- {^(-?[0-9\.]+)([a-z]+)$} $dim dummy qty units]} { rtf::warning "Bad dimension $dim" return 0 } if {![info exists rtf_twipsMap($units)]} { rtf::warning "Bad units $units" return 0 } set scale $rtf_twipsMap($units) return [expr round($qty * $scale)] } proc rtf::cvHalfpts {dim} { if {[regexp -- {^-?([0-9\.]+)$} $dim]} { return $dim } return [expr round([rtf::cvTwips $dim] / 10)] } ### ### Fonts ### global rtf_fonttbl rtf_numberFonts set rtf_fonttbl [join { {{\f0\froman\fcharset0\fprq2 Times New Roman;}} {{\f1\fswiss\fcharset0\fprq2 Arial;}} {{\f2\fmodern\fcharset0\fprq1 Courier New;}} } "" ] array set rtf::FontTable { roman 0 sans 1 mono 2 } set rtf_numberFonts 3 proc rtf::defineFont {id fontname args} { global rtf::FontTable rtf_numberFonts set rtf::FontTable($id) $rtf_numberFonts append rtf_fonttbl "{\\f$rtf_numberFonts\\fnil $fontname}" incr rtf_numberFonts } proc rtf::writeFontTable {} { #+%%% global rtf_fonttbl; rtf::bgroup rtf::write "\\fonttbl $rtf_fonttbl" rtf::egroup } ### ### Color table: ### Unlike the font table and stylesheet, there is no "define color" ### RTF control word. Instead, colors are specified by their ### position in the color table, which is simply a list of ### \red\green\blue values separated by semicolons. ### ### For consistency, RATFINK maps symbolic color names to ### color indexes in the same way as it does other parameters. ### array set rtf::ColorTable { black 1 white 2 } set rtf_numberColors 2 set rtf_colortbl {\red0\green0\blue0;\red255\green255\blue255;} proc rtf::defineColor {id spec} { global rtf::ColorTable rtf_colortbl rtf_numberColors # Parse color spec: #hhhhhh, #hhhhhhhhh, or #hhhhhhhhhhhh set hd {[0-9a-fA-F]}; set h2 "$hd$hd"; set h3 "$hd$hd$hd"; set h4 "$hd$hd$hd" if {[regexp "^#($h2)($h2)($h2)$" $spec - r g b]} { set m 0 } \ elseif {[regexp "^#($h3)($h3)($h3)$" $spec - r g b]} { set m 4 } \ elseif {[regexp "^#($h4)($h4)($h4)$" $spec - r g b]} { set m 8 } \ else { error "Bad color specification $spec" } foreach {v cs} {r red g green b blue} { scan [set $v] %x rgbval set rgbval [expr $rgbval >> $m] append rtf_colortbl "\\$cs$rgbval" } append rtf_colortbl ";\n" incr rtf_numberColors set rtf::ColorTable($id) $rtf_numberColors } proc rtf::writeColorTable {} { global rtf_colortbl; rtf::bgroup rtf::write "\\colortbl; $rtf_colortbl" rtf::egroup } ### ### Tab stops ### # The order of the control words must be ? ? , array set rtf_TabAttrs { Align {enum { {Left "tql"} {Right "tqr"} {Center "tqc"} {Decimal "tqdec"} }} Leaders {enum { {Dot "tldot"} {Hyphen "tlhyph"} {Under "tlul"} {Thick "tlth"} {Equal "tleq"} }} Position {dimension "tx"} } proc rtf::tabStops {name stops} { global rtf_TabTable if {[info exists rtf_TabTable($name)]} { rtf::warning "Redefining tab settings $name" } set rtf_TabTable($name) [rtf::ExpandTabStops $stops] } proc rtf::ExpandTabStops {stops} { global rtf_TabAttrs rtf_TabTable set result "" if {[regexp {^[a-zA-Z][-_a-zA-Z0-9]*$} $stops]} { if {[info exists rtf_TabTable($stops)]} { return $rtf_TabTable($stops) } else { rtf::warning "Tab settings <$stops> not defined" return "" } } foreach stop $stops { catch {unset tmp} set tmp(Position) [lindex $stop 0] array set tmp [lrange $stop 1 end]; append result \ [rtf::ExpandArray rtf_TabAttrs tmp "Align Leaders Position"] } return $result } ### ### Rule styles ### # Order of control words is: # , ?, ?, ? # == 'Style' is mandatory and must be first. # array set rtf_RuleAttrs { Style {enum { {Normal "brdrs"} {Thick "brdrth"} {Double "brdrdb"} {Shadow "brdrsh"} {Dot "brdrdot"} {Dash "brdrdash"} {Hairline "brdrhair"} }} Margin {dimension "brsp"} Thickness {dimension "brdrw"} Color {table rtf::ColorTable "brdrcf"} } proc rtf::ruleStyle {name attlist} { global rtf_RuleTable if {[info exists rtf_RuleTable($name)]} { rtf::warning "Redefining rule style $name" } set rtf_RuleTable($name) [rtf::ExpandRuleStyle $attlist] } proc rtf::ExpandRuleStyle {attlist} { global rtf_RuleAttrs rtf_RuleTable if {[llength $attlist] == 1} { global rtf_RuleTable if {[info exists rtf_RuleTable($attlist)]} { return $rtf_RuleTable($attlist) } else { rtf::warning "No rule style <$attlist> defined" return "" } } set tmp(Style) Normal; # 'Style' is mandatory; set default array set tmp $attlist return [rtf::ExpandArray rtf_RuleAttrs tmp "Style Margin Thickness Color"] } # Pre-defined rule styles: rtf::ruleStyle thin { Style Normal Thickness 0.75pt } rtf::ruleStyle thick { Style Normal Thickness 1.50pt } rtf::ruleStyle double { Style Double Thickness 0.75pt } ### ### Document-wide attributes ### array set rtf::DocAttrs { PaperWidth {dimension "paperw"} PaperHeight {dimension "paperh"} LeftMargin {dimension "margl"} RightMargin {dimension "margr"} TopMargin {dimension "margt"} BottomMargin {dimension "margb"} DefaultTabWidth {dimension "deftab"} TwoSide {flag "facingp"} MirrorMargins {flag "margmirror"} Landscape {flag "landscape"} FirstPageNumber {integer "pgnstart"} WidowControl {flag "widowctrl"} GutterWidth {dimension "gutter"} SaveAsRTF {flag "defformat"} Protection {enum { {Forms "formprot"} {Revisions "revisions\\revprot"} {Annotations "annotprot"} {AllProtected "allprot"} }} Hyphenate {boolean "hyphauto"} HyphenationHotZone {dimension "hyphhotz"} HyphenationLadderCount {integer "hyphconsec"} HyphenateAllCaps {boolean "hyphcaps"} FootnoteNumbering {enum { {Arabic "ftnnar"} {LCAlpha "ftnnalc"} {UCAlpha "ftnnauc"} {LCRoman "ftnnrlc"} {UCRoman "ftnnruc"} {Chicago "ftnnchi"} }} FootnoteLocation {enum { {EndOfSection "endnotes"} {EndOfDocument "enddoc"} }} FootnotePlacement {enum { {PageBottom "ftnbj"} {BeneathText "ftntj"} }} FirstFootnoteNumber {integer "ftnstart"} FootnoteRestart {enum { {Continuous "ftnrstcont"} {AtSection "ftnrestart"} {AtPage "ftnrstpg"} }} } # Add 'papersize' document options: set tmp {}; foreach psz { { A4 210mm 297mm } { A5 148mm 210mm } { B5 176mm 250mm } { Letter 8.5in 11in } { Legal 8.5in 14in } { Executive 7.25in 10.5in } } { set pszn [lindex $psz 0] set pszw [rtf::cvTwips [lindex $psz 1]] set pszh [rtf::cvTwips [lindex $psz 2]] lappend tmp [list $pszn "paperw$pszw\\paperh$pszh"] } set rtf::DocAttrs(PaperSize) "enum { $tmp }" proc rtf::documentFormat {params} { global rtf_DocumentFormat array set rtf_DocumentFormat $params } proc rtf::writeDocumentFormat {} { global rtf_DocumentFormat if {[info exists rtf_DocumentFormat]} { rtf::write [rtf::ExpandArray rtf::DocAttrs rtf_DocumentFormat] rtf::write " " } } ### ### Information group ### proc rtf::writeInformationGroup {} { global rtf_state # %%% fake it for now... rtf::bgroup rtf::write "\\info" rtf::bgroup rtf::write "\\doccomm Created by RATFINK/Cost" if {$rtf_state(docTitle) != ""} { rtf::write "\\title $rtf_state(docTitle)" } rtf::egroup rtf::egroup } ### ### Output ### global rtf_fp set rtf_fp stdout proc rtf::write {text} { global rtf_fp puts -nonewline $rtf_fp $text } proc rtf::bgroup {} { rtf::write \{ } proc rtf::egroup {} { rtf::write \} } # Note: # In RTF, the way to get a literal "{" is with "\{". # In Winhelp-style RTF, though, "\{" itself is sometimes magic! # So, we insert some extra junk (an invisible period, \v .) after # each literal open-bracket to prevent HCW from misinterpreting # it as a WINHELP directive. # if {[array exists COST]} { substitution rtf::Escape { "{" "\\{{\\v .}" "}" "\\}" "\\" "\\\\" "\t" "\\tab " "\n" " " "`" "\\lquote " "'" "\\rquote " "``" "\\ldblquote " "''" "\\rdblquote " "--" "\\endash " "---" "\\emdash " } substitution rtf::EscapeLineSpecific { "{" "\\{{\\v .}" "}" "\\}" "\\" "\\\\" "\t" "\\tab " "\n" "\\line\n" } } else { proc rtf::Escape {text} { regsub -all {[{}\\]} $text {\\&} text return $text } } proc rtf::text {text} { global rtf_state if {!$rtf_state(inpara) && [string length [string trim $text]]} { rtf::startPara [rtf::currentParaStyle] } rtf::write [rtf::Escape $text] } proc rtf::insert {data} { global rtf_state if {!$rtf_state(inpara)} {rtf::startPara [rtf::currentParaStyle]} rtf::write $data } # may be overridden by applications: proc rtf::currentParaStyle {} { rtf::warning "No current paragraph style defined" return "-" } array set rtfSpecial { Tab "\\tab " LineBreak "\\line " PageBreak "\\page " ColumnBreak "\\column " EmDash "\\emdash " EnDash "\\endash " EmSpace "{\\emspace }" EnSpace "{\\enspace }" Bullet "\\bullet " LSQuote "\\lquote " RSQuote "\\rquote " LDQuote "\\ldblquote " RDQuote "\\rdblquote " PageNumber "\\chpgn " SectionNumber "\\sectnum " FootnoteNumber "\\chftn " } # Ellipsis "{\\expndtw20 ...}" ??? # There is reportedly an ellipsis character in the Symbol character set... # # # Built-in bitmaps for Winhelp (see [DOH], p. 236) # array set rtfWinhelpSpecial { Bullet "\\{bmct bullet.bmp\\}" EmDash "\\{bmct emdash.bmp\\}" Shortcut "\\{bmct shortcut.bmp\\}" Onestep "\\{bmct onestep.bmp\\}" Open "\\{bmct open.bmp\\}" Closed "\\{bmct closed.bmp\\}" Document "\\{bmct document.bmp\\}" Do-It "\\{bmct do-it.bmp\\}" Chiclet "\\{bmct chiclet.bmp\\}" PRCArrow "\\{bmct prcarrow.bmp\\}" } proc rtf::special {code} { global rtfSpecial if {[info exists rtfSpecial($code)]} { rtf::insert $rtfSpecial($code) } else { rtf::warning "Unrecognized special <$code>" } } proc rtf::tab {} { rtf::write "\\tab " } proc rtf::lineBreak {} { rtf::write "\\line " } proc rtf::pageBreak {} { rtf::write "\\page " } proc rtf::columnBreak {} { rtf::write "\\column " } ### ### Document structure ### global rtf_state array set rtf_state { inpara 0 insection 0 intable 0 inrow 0 incell 0 cellno 0 diversion "" winhelpMode 0 docTitle "" outputFile {} contentsFile {} tocfp {} toclevel 1 browseseq "" topicno 0 currentTopic "" intopic 0 inlink 0 } proc rtf::start {} { global rtf_state rtf_fp fconfigure $rtf_fp -buffering full if {$rtf_state(outputFile) != ""} { set rtf_fp [open $rtf_state(outputFile) w] fconfigure $rtf_fp -buffering full } if {$rtf_state(winhelpMode)} { rtf::startContentsFile } rtf::bgroup # Header: rtf::write "\\rtf1\\ansi\\deff0" rtf::writeFontTable #- fileTable rtf::writeColorTable rtf::writeStyleSheet #- revisionTable # Document: rtf::writeInformationGroup rtf::writeDocumentFormat # section text follows... } proc rtf::end {} { global rtf_state rtf_fp rtf::egroup if {$rtf_state(outputFile) != ""} { close $rtf_fp } if {$rtf_state(winhelpMode)} { rtf::endContentsFile } } proc rtf::startSection {{style -}} { global rtf_state rtf_styleSheet rtf::endSection rtf::write "\\sectd" if {$style != "-"} { if {![info exists rtf_styleSheet($style,DEF)] \ || $rtf_styleSheet($style,TYPE) != "SECT"} { rtf::warning "Style <$style> not a section style" } else { rtf::write $rtf_styleSheet($style,DEF) } rtf::write " " } set rtf_state(insection) 1 } proc rtf::endSection {} { global rtf_state if {$rtf_state(insection)} { rtf::write "\\sect " } set rtf_state(insection) 0 } proc rtf::startPara {style} { global rtf_styleSheet rtf_state if {$rtf_state(inpara)} { rtf::endPara } rtf::write "\\pard\\plain " if {$rtf_state(intable)} { rtf::write "\\intbl " } if {[info exists rtf_styleSheet($style,DEF)]} { if {$rtf_styleSheet($style,TYPE) != "PARA"} { rtf::warning "$style not a paragraph style" } else { rtf::write "\\s$rtf_styleSheet($style,NUM)" } rtf::write "$rtf_styleSheet($style,DEF) " } elseif {$style != "-"} { rtf::undefined style $style } set rtf_state(inpara) 1 } proc rtf::endPara {} { global rtf_state if {$rtf_state(inpara)} { rtf::write "\\par\n" } set rtf_state(inpara) 0 } proc rtf::startPhrase {style} { rtf::bgroup rtf::setCharStyle $style } proc rtf::endPhrase {} { rtf::egroup } proc rtf::setCharStyle {style} { global rtf_styleSheet rtf_state if {!$rtf_state(inpara)} { rtf::startPara [rtf::currentParaStyle] } if {[info exists rtf_styleSheet($style,DEF)]} { if {$rtf_styleSheet($style,TYPE) != "CHAR"} { rtf::warning "$style not a character style" } else { rtf::write "\\cs$rtf_styleSheet($style,NUM)" } rtf::write "$rtf_styleSheet($style,DEF) " } else { rtf::warning "No definition for character style <$style>" } } ### ### Tables ### # # Different ways to specify number and widths of columns: # # + number of columns and total width (equal-sized columns) # + list of relative widths and total width # + list of absolute widths # proc rtf::tableRelativeWidths {colwidths} { global rtf_tableSettings rtf_cellPositions set tablewidth [rtf::cvTwips $rtf_tableSettings(width)] set sumwidths 0 foreach colwidth $colwidths { incr sumwidths $colwidth } set colend 0 set colnum 0 foreach colwidth $colwidths { incr colend [expr round(($tablewidth*$colwidth)/$sumwidths)] incr colnum set rtf_cellPositions($colnum) $colend } set rtf_tableSettings(numcols) $colnum } proc rtf::TableAbsoluteWidths {colwidths} { global rtf_tableSettings rtf_cellPositions set colend 0 set colnum 0 foreach colwidth $colwidths { incr colend [rtf::cvTwips $colwidth] incr colnum set rtf_cellPositions($colnum) $colend } set rtf_tableSettings(width) $colend set rtf_tableSettings(numcols) $colnum } proc rtf::TableEqualWidths {numcols} { global rtf_tableSettings rtf_cellPositions set rtf_tableSettings(numcols) $numcols set tablewidth [rtf::cvTwips $rtf_tableSettings(width)] set colnum 0 while {$colnum < $numcols} { incr colnum set rtf_cellPositions($colnum) \ [expr round($tablewidth*$colnum/$numcols)] } } proc rtf::startTable {args} { global rtf_state rtf_tableSettings rtf_tableDefaults if {$rtf_state(intable)} { rtf::warning "Already in table" return; } array set rtf_tableSettings [array get rtf_tableDefaults] catch {unset rtf_cellPositions} set option "" foreach arg $args { if {$option == ""} { set option $arg; continue; } switch -- $option { -width { set rtf_tableSettings(width) $arg } -align { set rtf_tableSettings(rowalign) $arg } -numcols { rtf::TableEqualWidths $arg } -relwidths { rtf::TableRelativeWidths $arg } -abswidths { rtf::TableAbsoluteWidths $arg } -toprule { set rtf_tableSettings(toprule) $arg } -colsep { set rtf_tableSettings(colsep) $arg } -rowsep { set rtf_tableSettings(rowsep) $arg } -frame { set rtf_tableSettings(frame) $arg set rtf_tableSettings(toprule) $arg } default { rtf::warning "rtf::startTable: bad option $option" } } set option "" } if {$option != ""} { rtf::warning "rtf::startTable: no argument for option $option" } set rtf_tableSettings(botrule) $rtf_tableSettings(rowsep) array set rtf_state { intable 1 inrow 0 incell 0 cellno 0 } } proc rtf::startRow {args} { global rtf_state rtf_tableSettings if {!$rtf_state(intable)} { rtf::warning "Not in a table" } if {$rtf_state(inrow)} { rtf::endRow } set isheading 0 set colspans "" set option "" foreach arg $args { if {$option == ""} { set option $arg; continue; } switch -- $option { -colspans { set colspans $arg } -toprule { set rtf_tableSettings(toprule) $arg } -botrule { set rtf_tableSettings(botrule) $arg } -colsep { set rtf_tableSettings(colsep) $arg } -heading { set isheading $arg } default { rtf::warning "rtf::startRow: bad option $option" } } set option "" } if {$option != ""} { rtf::warning "rtf::startRow: no argument for option $option" } set rowatts [list \ RowGap $rtf_tableSettings(rowgap) \ Align $rtf_tableSettings(rowalign) ] if {$isheading} { lappend rowatts HeadingRow $isheading } rtf::write "\\trowd[rtf::ExpandAttributes rtf::TableAttrs $rowatts]\n" if {$colspans != ""} { set rtf_tableSettings(numcells) [llength $colspans] set colno 1 foreach span $colspans { rtf::cellDef $colno $span incr colno $span } } else { set rtf_tableSettings(numcells) $rtf_tableSettings(numcols) for {set colno 1} {$colno <= $rtf_tableSettings(numcols)} {incr colno} { rtf::cellDef $colno } } rtf::write "\n" array set rtf_state { inrow 1 incell 0 cellno 0 } } proc rtf::cellDef {colno {span 1}} { global rtf_tableSettings rtf_cellPositions set start $colno set end [expr $colno + $span - 1] if {$end > $rtf_tableSettings(numcols)} { rtf::warning "Cell spans too many columns" return } if {$start == 1} { set border(Left) $rtf_tableSettings(frame) } else { set border(Left) $rtf_tableSettings(colsep) } if {$end == $rtf_tableSettings(numcols)} { set border(Right) $rtf_tableSettings(frame) } else { set border(Right) $rtf_tableSettings(colsep) } set border(Top) $rtf_tableSettings(toprule) set border(Bottom) $rtf_tableSettings(botrule) set cellAtts {} foreach b {Top Bottom Left Right} { if {$border($b) != "-"} { lappend cellAtts "CellBorder$b" $border($b) } } # cellx must be last lappend cellAtts CellPosition $rtf_cellPositions($end) rtf::write [rtf::ExpandAttributes rtf::TableAttrs $cellAtts] } # ... need to distinguish between cells which contain paragraphs # ... and those which only contain inline stuff; # ... for the latter apply paragraph properties when the cell starts proc rtf::startCell {{style -}} { global rtf_state if {$rtf_state(incell)} { rtf::endCell } set rtf_state(incell) 1 incr rtf_state(cellno) rtf::startPara $style } proc rtf::endCell {} { global rtf_state if {!$rtf_state(incell)} { rtf::warning "Not in a cell"; return } rtf::write "\\cell\n" array set rtf_state { inpara 0 incell 0 } } proc rtf::endRow {} { global rtf_state rtf_tableSettings if {!$rtf_state(inrow)} { rtf::warning "Not in a table row" return } if {$rtf_state(incell)} rtf::endCell if {$rtf_state(cellno) != $rtf_tableSettings(numcells)} { rtf::warning \ "$rtf_state(cellno) cells; should be $rtf_tableSettings(numcells)" } rtf::write "\\row\n" set rtf_state(inrow) 0 set rtf_tableSettings(toprule) $rtf_tableSettings(botrule) set rtf_tableSettings(botrule) $rtf_tableSettings(rowsep) } proc rtf::endTable {} { global rtf_state if {!$rtf_state(intable)} { rtf::warning "Not in a table" return } if {$rtf_state(inrow)} { rtf::endRow } # without this, Word sometimes crashes... rtf::write "\\pard\\par\n" set rtf_state(intable) 0 } ### ### Bookmarks ### proc rtf::startBookmark {name} { rtf::bgroup rtf::write "{\\*\\bkmkstart $name}" rtf::egroup } proc rtf::endBookmark {name} { rtf::bgroup rtf::write "{\\*\\bkmkend $name}" rtf::egroup } ### ### Fields ### proc rtf::startField {inst} { rtf::bgroup rtf::write "\\field" rtf::bgroup rtf::write "\\*\\fldinst " rtf::write [rtf::Escape $inst] rtf::egroup rtf::bgroup rtf::write "\\fldrslt " } proc rtf::endField {} { rtf::egroup rtf::egroup } proc rtf::insertField {inst {rslt ""}} { rtf::write \ "{\\field{\\*\\fldinst [rtf::Escape $inst]}{\\fldrslt [rtf::Escape $rslt]}}" } # %%% note: syntax productions in spec disagree with examples, # %%% what Word actually does, and common sense. ### ### Destination groups ### array set rtf_Destinations { Header "header" LeftHeader "headerl" RightHeader "headerr" FirstPageHeader "headerf" Footer "footer" FirstPageFooter "footerf" LeftFooter "footerl" RightFooter "footerr" Footnote "footnote" Endnote "footnote\\ftnalt" } # %%% Should stack diversions, current state &c; # %%% NB: For RTF magic footnotes, use rtf::magicFootnote instead; # %%% the Winhelp compiler doesn't like the "\*" control sequence. # proc rtf::divert {diversion} { global rtf_state rtf_Destinations rtf_savedState if {$rtf_state(diversion) != ""} { rtf::warning "Diversion $diversion within $rtf_state(diversion)" } if {![info exists rtf_Destinations($diversion)]} { rtf::warning "Bad destination $diversion" return } rtf::bgroup rtf::write "\\*\\$rtf_Destinations($diversion) " array set rtf_savedState [array get rtf_state] array set rtf_state {inpara 0 insection 0 incell 0 inrow 0 intable 0} set rtf_state(diversion) $diversion return } proc rtf::undivert {} { global rtf_state rtf_savedState if {$rtf_state(diversion) == ""} { rtf::warning "No current diversion" return } rtf::egroup array set rtf_state [array get rtf_savedState] unset rtf_savedState set rtf_state(diversion) "" return; } ### ### Winhelp stuff: ### # # Useful reference: # [DOH] "Developing Online Help for Windows 95", # Scott Boggan, David Farkas, and Joe Welinske # # rtf::winhelpMode -- # Sets things up for generating WINHELP-style RTF. # Call this before defining any stylesheet entries. # proc rtf::winhelpMode {args} { global rtf_state if {$rtf_state(winhelpMode)} { # already set return } set rtf_state(winhelpMode) 1 # # Remove unsupported CharAttrs, ParaAttrs, and SectAttrs entries: # %%% Missing quite a few ... # global rtf::CharAttrs rtf::ParaAttrs rtf::SectAttrs foreach {old new} { KeepWithNext Banner KeepTogether NoWrap } { set rtf::ParaAttrs($new) [ set rtf::ParaAttrs($old)] unset rtf::ParaAttrs($old) } unset rtf::SectAttrs proc ::rtf::SectStyle {args} { rtf::warning "rtf::SectStyle can't be used in WINHELP mode" } # # Add WINHELP-specific special characters: # global rtfSpecial rtfWinhelpSpecial array set rtfSpecial [array get rtfWinhelpSpecial] # %%% TODO: disable rtf::pageBreak, other stuff that shouldn't # %%% appear in WINHELP-format RTF. } # rtf::winhelpOnly -- # Internal utility. Make sure we're in Winhelp mode # proc rtf::winhelpOnly {{msg ""}} { global rtf_state if {!$rtf_state(winhelpMode)} { error "must be in Winhelp mode: $msg" } } ### ### WINHELP "magic footnotes": ### array set rtf_magicFootnotes { topicID # title $ browseseq + keyword K alink A comment @ helpmacro ! windowType > } proc rtf::magicFootnote {key value} { global rtf_magicFootnotes rtf::write $rtf_magicFootnotes($key) rtf::divert Footnote rtf::write $value rtf::undivert } proc rtf::startBrowseSequence {bsname} { global rtf_state rtf::winhelpOnly rtf::StartBrowseSequence if {![info exists rtf_state(bs.$bsname.seqno)]} { set rtf_state(bs.$bsname.seqno) 0 } if {$rtf_state(browseseq) != ""} { rtf::warning "Cannot nest browse sequences ($rtf_state(browseseq)) in $bsname" } set rtf_state(browseseq) $bsname } proc rtf::endBrowseSequence {} { global rtf_state set rtf_state(browseseq) "" } # # rtf::startTopic -- # %%% Document me # %%% "keywords" (K footnotes) are really more like "index entries" # %%% "A footnotes" are more like "keywords" # proc rtf::startTopic {args} { global rtf_state incr rtf_state(topicno) array set pageOpts { id "" title "" keywords {} tocentry 1 windowType {} } set pageOpts(browseseq) "$rtf_state(browseseq)" foreach {option value} $args { switch -- $option { -topicID { set pageOpts(id) $value } -title { set pageOpts(title) $value } -keyword { lappend pageOpts(keywords) $value } -keywords { set pageOpts(keywords) [concat $pageOpts(keywords) $value] } -browseSequence { set pageOpts(browseseq) $value } -windowType { set pageOpts(windowType) $value } default { error "Unrecognized option $option" } } } if {$pageOpts(id) == ""} { set pageOpts(id) "TOPIC$rtf_state(topicno)" } set bsname $pageOpts(browseseq) if {$bsname != ""} { if {![info exists rtf_state(bs.$bsname.seqno)]} { # %%% rtf::startBrowseSequence; end at end set rtf_state(bs.$bsname.seqno) 0 } set bseqno [incr rtf_state(bs.$bsname.seqno)] } # Make sure we're not in the middle of anything else... # if {$rtf_state(inpara)} { rtf::endPara } if {$rtf_state(intopic)} { rtf::endTopic } # Generate topic header: # rtf::pageBreak rtf::magicFootnote topicID $pageOpts(id) if {$pageOpts(title) != ""} { rtf::magicFootnote title $pageOpts(title) } if {$pageOpts(windowType) != ""} { rtf::magicFootnote windowType $pageOpts(windowType) } foreach kw $pageOpts(keywords) { rtf::magicFootnote keyword $kw } #if {[llength $pageOpts(keywords)] != 0} { # set keywordSep ";" # rtf::magicFootnote keyword [join $pageOpts(keywords) $keywordSep] #} if {$bsname != ""} { rtf::magicFootnote browseseq "$bsname:[format %05d $bseqno]" } # Add contents line: # if {$pageOpts(tocentry)} { set linkTarget $pageOpts(id) if {$pageOpts(windowType) != ""} { append linkTarget ">$pageOpts(windowType)" } rtf::contentsLine $pageOpts(title) $linkTarget } set rtf_state(intopic) 1 set rtf_state(currentTopic) $pageOpts(id) } proc rtf::endTopic {} { global rtf_state if {$rtf_state(inlink)} { rtf::warning "Topic ended inside a link" } set rtf_state(intopic) 0 set rtf_state(currentTopic) "" } proc rtf::currentTopic {} { return $::rtf_state(currentTopic) } # # startLink/endLink # %%% TODO: keep track of defined/referenced topics, make sure they match. # %%% TODO: handle pop-up targets, other target types (ALINK,secondary win,&c) # %%% Possibly: call this startJump for consistency w/WINHELP docs. # # WINHELP-RTF syntax for link targets: # ['%'|'*']? ( topicID ['@' helpFile ]? ['>' targetWindow]? # | '!' helpMacro ) # '*' at beginning of target spec makes link text appear in normal color, # '%' does the same and also removes underlining. # # As a special case, if the link target starts with '^', # rtf::startLink generates a pop-up link. (In WINHELP-RTF, # this is specified by single-underlining the link text). # # rtf::startKLink is like rtf::startLink, but it makes a keyword-link instead. # proc rtf::startLink {linkTarget} { global rtf_state rtf::winhelpOnly if { $rtf_state(inlink) } { rtf::warning "Nested link!" } if {[string match "^*" $linkTarget]} { # Pop-up link set ulstyle Single set linkTarget [string range $linkTarget 1 end] } else { # Normal link set ulstyle Double } rtf::bgroup rtf::write "[rtf::ExpandAttributes rtf::CharAttrs [list Underline $ulstyle ]] " set rtf_state(LinkTarget) $linkTarget set rtf_state(inlink) 1 } proc rtf::startKLink {keyword} { global rtf_state rtf::winhelpOnly if { $rtf_state(inlink) } { rtf::warning "Nested link!" } rtf::bgroup rtf::write "[rtf::ExpandAttributes rtf::CharAttrs [list Underline Double]] " set rtf_state(LinkTarget) "!KLink($keyword)" set rtf_state(inlink) 1 } proc rtf::endLink {} { global rtf_state rtf::egroup rtf::bgroup rtf::write "[rtf::ExpandAttributes rtf::CharAttrs { Hidden 1 }] " rtf::write $rtf_state(LinkTarget) rtf::egroup unset rtf_state(LinkTarget) set rtf_state(inlink) 0 } # rtf::uriLink uri -- # Returns a link target (suitable for rtf::startLink) # that references the specified URI. # proc rtf::uriLink {uri} { return "!ExecFile(`$uri',,0,)" # "!ShellExecute($uri)" is supposed to work, but doesn't } # rtf::button label macro1 ... macroN # Generates an in-line pushbutton with label "label" # that invokes Winhelp macros macro1 ... macroN. # If "label" is empty, generates a square, blank button. # See [DOH], pp.290-292. # proc rtf::button {label args} { rtf::winhelpOnly rtf::write "\\{button [rtf::Escape $label],[join $args :]\\}" } ### ### Winhelp contents file: ### proc rtf::startContentsFile {} { global rtf_state if {!$rtf_state(winhelpMode) || $rtf_state(contentsFile) == ""} { return } set fp [open $rtf_state(contentsFile) w] if {$rtf_state(docTitle) != ""} { puts $fp ":TITLE $rtf_state(docTitle)" # %%% Default: specified in .HPJ file } # %%% this is not quite right... set helpFilename "[file tail [file rootname $rtf_state(contentsFile)]].HLP" puts $fp ":BASE $helpFilename" set rtf_state(tocfp) $fp } proc rtf::endContentsFile {} { global rtf_state if {$rtf_state(contentsFile) != ""} { close $rtf_state(tocfp) } } proc rtf::contentsLine {title {linkTarget ""}} { global rtf_state rtf::winhelpOnly "rtf::contentsLine" set fp $rtf_state(tocfp) if {$fp == ""} { return } regsub -all {\n} $title " " title if {[string match "^*" $linkTarget]} { rtf::warning "Pop-up window referenced in table of contents" set linkTarget [string range $linkTarget 1 end] } if {$linkTarget != ""} { puts $fp "$rtf_state(toclevel) $title=$linkTarget" } else { puts $fp "$rtf_state(toclevel) $title" } } proc rtf::contentsLevel {n} { global rtf_state incr rtf_state(toclevel) $n } ### ### RATFINK configuration options: ### proc rtf::configure {args} { global rtf_state foreach {option value} $args { switch -- $option { -outputFile { set rtf_state(outputFile) $value } -contentsFile { set rtf_state(contentsFile) $value } -mapFile { rtf::warning "-mapFile not yet implemented" } -docTitle { set rtf_state(docTitle) $value } default { error "Unrecognized option $option = $value" } } } } #*EOF*