├── .gitignore ├── COPYING ├── README.md ├── autoload.php ├── examples ├── example.1 └── example.html ├── lib ├── Block │ ├── DefinitionList.php │ ├── EndPreformatted.php │ ├── IP.php │ ├── P.php │ ├── Preformatted.php │ ├── RE.php │ ├── RS.php │ ├── SY.php │ ├── Section.php │ ├── TH.php │ ├── TP.php │ ├── TS.php │ ├── TabTable.php │ ├── Template.php │ ├── Text.php │ ├── ad.php │ ├── ce.php │ ├── fc.php │ └── ti.php ├── Blocks.php ├── DOM.php ├── Indentation.php ├── Inline │ ├── AlternatingFont.php │ ├── EQ.php │ ├── FontOneInputLine.php │ ├── Link.php │ ├── LinkEnd.php │ ├── MR.php │ ├── OP.php │ ├── PS.php │ ├── URL.php │ ├── VerticalSpace.php │ └── ft.php ├── Man.php ├── Manner.php ├── Massage │ ├── Block.php │ ├── Body.php │ ├── DIV.php │ ├── DL.php │ ├── DT.php │ ├── HTMLList.php │ ├── Indents.php │ ├── LI.php │ ├── P.php │ ├── PRE.php │ ├── Remap.php │ └── Tidy.php ├── Node.php ├── PreformattedOutput.php ├── Preprocessor.php ├── Replace.php ├── Request.php ├── Request │ ├── Skippable.php │ └── Unhandled.php ├── Roff.php ├── Roff │ ├── Alias.php │ ├── Char.php │ ├── Comment.php │ ├── Condition.php │ ├── Glyph.php │ ├── Loop.php │ ├── Macro.php │ ├── Register.php │ ├── Rename.php │ ├── Skipped.php │ ├── StringRequest.php │ ├── Template.php │ ├── Translation.php │ ├── Unit.php │ ├── am.php │ ├── asRequest.php │ ├── cc.php │ ├── de.php │ ├── di.php │ ├── doRequest.php │ ├── ec.php │ ├── eo.php │ ├── nop.php │ └── returnRequest.php ├── Text.php └── TextContent.php ├── manner.php ├── run_tests.php └── tests ├── 0in.1 ├── 0in.1.html ├── BR.1 ├── BR.1.html ├── FONT.1 ├── FONT.1.html ├── FontOneInputLine.1 ├── FontOneInputLine.1.html ├── IPs.1 ├── IPs.1.html ├── MT.1 ├── MT.1.html ├── NOP.1 ├── NOP.1.html ├── SbC2.1 ├── SbC2.1.html ├── SbCylinder.3iv ├── SbCylinder.3iv.html ├── SoXtWalkViewer-b.3 ├── SoXtWalkViewer-b.3.html ├── SoXtWalkViewer.3 ├── SoXtWalkViewer.3.html ├── ZN.1 ├── ZN.1.html ├── apropos.1 ├── apropos.1.html ├── atop.1 ├── atop.1.html ├── b.1 ├── b.1.html ├── bash_fonts.1 ├── bash_fonts.1.html ├── bash_sm.1 ├── bash_sm.1.html ├── br_in_tp.1 ├── br_in_tp.1.html ├── bull.1 ├── bull.1.html ├── c.3 ├── c.3.html ├── callerid.conf.5 ├── callerid.conf.5.html ├── cc.1 ├── cc.1.html ├── cef5conv.1 ├── cef5conv.1.html ├── chars.1 ├── chars.1.html ├── ci.2 ├── ci.2.html ├── comment_after_font.1 ├── comment_after_font.1.html ├── comp.1 ├── comp.1.html ├── cpupower-monitor.1 ├── cpupower-monitor.1.html ├── customVb.1 ├── customVb.1.html ├── digit_space.1 ├── digit_space.1.html ├── dir.1 ├── dir.1.html ├── eh.1 ├── eh.1.html ├── ep.1 ├── ep.1.html ├── eqn.1 ├── eqn.1.html ├── ex.1 ├── ex.1.html ├── fortune.6 ├── fortune.6.html ├── gawk.1b ├── gawk.1b.html ├── gcc.1 ├── gcc.1.html ├── gdiffmk.1 ├── gdiffmk.1.html ├── gifsicle.1 ├── gifsicle.1.html ├── glue-validator.1 ├── glue-validator.1.html ├── gmusicbrowser.1 ├── gmusicbrowser.1.html ├── gpp.1 ├── gpp.1.html ├── groff_mm.7 ├── groff_mm.7.html ├── hg.1 ├── hg.1.html ├── indentation-a.1 ├── indentation-a.1.html ├── innxbatch.8 ├── innxbatch.8.html ├── iostat.1 ├── iostat.1.html ├── ksh.1 ├── ksh.1.html ├── links2.1 ├── links2.1.html ├── listA.1 ├── listA.1.html ├── logical_or.1 ├── logical_or.1.html ├── lsmcli.1 ├── lsmcli.1.html ├── mdadm.8 ├── mdadm.8.html ├── mplayer.1 ├── mplayer.1.html ├── nawk.1 ├── nawk.1.html ├── nf.1 ├── nf.1.html ├── ovn-sb.5 ├── ovn-sb.5.html ├── pmcpp.1 ├── pmcpp.1.html ├── pmdapipe.1 ├── pmdapipe.1.html ├── portage.5 ├── portage.5.html ├── pp.1 ├── pp.1.html ├── pp_in_pre.1 ├── pp_in_pre.1.html ├── prefontspace.2 ├── prefontspace.2.html ├── proc.5 ├── proc.5.html ├── ps.1 ├── ps.1.html ├── rc.4 ├── rc.4.html ├── rc.4b ├── rc.4b.html ├── rc.4c ├── rc.4c.html ├── rc.4d ├── rc.4d.html ├── rc.4e ├── rc.4e.html ├── rc.4f ├── rc.4f.html ├── re_syntax.n ├── re_syntax.n.html ├── repl.1 ├── repl.1.html ├── roff.7 ├── roff.7.html ├── rs.1 ├── rs.1.html ├── screen.1 ├── screen.1.html ├── sh.1 ├── sh.1.html ├── simple.1 ├── simple.1.html ├── soft_hyphen.1 ├── soft_hyphen.1.html ├── sox.1 ├── sox.1.html ├── sox.1b ├── sox.1b.html ├── spaces.1 ├── spaces.1.html ├── tcpdump_rses.1 ├── tcpdump_rses.1.html ├── ti.1 ├── ti.1.html ├── tp.1 ├── tp.1.html ├── tpthennewline.1 ├── tpthennewline.1.html ├── tr.1 ├── tr.1.html ├── trailingTP.1 ├── trailingTP.1.html ├── ttylink.1 ├── ttylink.1.html ├── units.1 ├── units.1.html ├── url.1 ├── url.1.html ├── warnquota.conf.5 ├── warnquota.conf.5.html ├── while.1 ├── while.1.html ├── xpaenv.n ├── xpaenv.n.html ├── xterm.1 ├── xterm.1.html ├── zct.1 └── zct.1.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | /** 23 | * An example of a project-specific implementation. 24 | * 25 | * After registering this autoload function with SPL, the following line 26 | * would cause the function to attempt to load the \Foo\Bar\Baz\Qux class 27 | * from /path/to/project/src/Baz/Qux.php: 28 | * 29 | * new \Foo\Bar\Baz\Qux; 30 | * 31 | * @param string $class The fully-qualified class name. 32 | * @return void 33 | */ 34 | spl_autoload_register( 35 | function ($class) { 36 | // project-specific namespace prefix 37 | $prefix = 'Manner\\'; 38 | 39 | // base directory for the namespace prefix 40 | $base_dir = __DIR__ . '/lib/'; 41 | 42 | // does the class use the namespace prefix? 43 | $len = strlen($prefix); 44 | if (strncmp($prefix, $class, $len) !== 0) { 45 | // no, move to the next registered autoloader 46 | return; 47 | } 48 | 49 | // get the relative class name 50 | $relative_class = mb_substr($class, $len); 51 | 52 | // replace the namespace prefix with the base directory, replace namespace 53 | // separators with directory separators in the relative class name, append 54 | // with .php 55 | $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; 56 | 57 | // if the file exists, require it 58 | if (file_exists($file)) { 59 | require $file; 60 | } 61 | } 62 | ); -------------------------------------------------------------------------------- /examples/example.1: -------------------------------------------------------------------------------- 1 | .TH EXAMPLE "1" "June 2024" "manner" "Example Man Page" 2 | 3 | .SH NAME 4 | example \- sample man page 5 | 6 | .SH SYNOPSIS 7 | .B example 8 | [\fI\,OPTION\/\fR]... 9 | 10 | .SH DESCRIPTION 11 | .\" This is a comment 12 | .PP 13 | Not a real command or man page! 14 | .TP 15 | \fB\-a\fR, \fB\-\-all\fR 16 | a sample option 17 | .TP 18 | \fB\-b\fR, \fB\-\-ball\fR 19 | another sample option 20 | .PP 21 | 22 | .TS 23 | tab(@); 24 | l l. 25 | T{ 26 | Column 1 27 | T}@T{ 28 | Column 2 29 | T} 30 | _ 31 | T{ 32 | row1 33 | T}@T{ 34 | This is some tabular date 35 | T} 36 | T{ 37 | this is the second row 38 | T}@T{ 39 | translated to an HTML 40 | T} 41 | .TE 42 | 43 | 44 | .SH SEE ALSO 45 | .IP \(bu 46 | First thing to see: 47 | .UR https://example.com/something 48 | Some Project 49 | .UE . 50 | .IP \(bu 51 | Second thing to see. 52 | 53 | .SH AUTHORS 54 | This was written by 55 | .MT author1@example.com 56 | Author 1 57 | .ME 58 | and 59 | .MT author2@example.com 60 | Author 2 61 | .ME . 62 | -------------------------------------------------------------------------------- /examples/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | EXAMPLE 10 | 11 |

EXAMPLE

12 |
13 |

NAME

14 |

example - sample man page

15 |
16 |
17 |

SYNOPSIS

18 |

example [OPTION]...

19 |
20 |
21 |

DESCRIPTION

22 |

Not a real command or man page!

23 |
24 |
-a, --all
25 |

a sample option

26 |
-b, --ball
27 |

another sample option

28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
Column 1Column 2
row1This is some tabular date
this is the second rowtranslated to an HTML <table>
43 | 44 |
45 |

SEE ALSO

46 | 53 |
54 |
55 |

AUTHORS

56 |

57 | This was written by Author 1 and 58 | Author 2. 59 |

60 |
61 | 62 | -------------------------------------------------------------------------------- /lib/Block/DefinitionList.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | 26 | class DefinitionList 27 | { 28 | 29 | public static function getParentDL(DOMElement $parentNode): ?DOMElement 30 | { 31 | do { 32 | $tag = $parentNode->tagName; 33 | if ($tag === 'dl') { 34 | return $parentNode; 35 | } 36 | if ($tag === 'body' || ($tag === 'div' && !$parentNode->hasAttribute('remap'))) { 37 | return null; 38 | } 39 | } while ($parentNode = $parentNode->parentNode); 40 | 41 | return null; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /lib/Block/EndPreformatted.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use Manner\Node; 26 | use Manner\PreformattedOutput; 27 | 28 | class EndPreformatted implements Template 29 | { 30 | 31 | public static function checkAppend( 32 | DOMElement $parentNode, 33 | array &$lines, 34 | array $request, 35 | bool $needOneLineOnly = false 36 | ): ?DOMElement { 37 | array_shift($lines); 38 | 39 | if ($pre = Node::ancestor($parentNode, 'pre')) { 40 | PreformattedOutput::reset(); 41 | 42 | return $pre->parentNode; 43 | } else { 44 | return null; 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /lib/Block/IP.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use Exception; 26 | use Manner\Blocks; 27 | use Manner\Indentation; 28 | use Manner\Man; 29 | use Manner\Roff; 30 | use Manner\TextContent; 31 | 32 | /* 33 | * .de1 IP 34 | * . ie !\\n[.$] \{\ 35 | * . ps \\n[PS]u 36 | * . vs \\n[VS]u 37 | * . ft R 38 | * . sp \\n[PD]u 39 | * . ne (1v + 1u) 40 | * . in (\\n[an-margin]u + \\n[an-prevailing-indent]u) 41 | * . ns 42 | * . \} 43 | * . el \{\ 44 | * . ie (\\n[.$] - 1) .TP "\\$2" 45 | * . el .TP 46 | * \&\\$1 47 | * . \} 48 | * .. 49 | * 50 | */ 51 | 52 | class IP implements Template 53 | { 54 | 55 | /** 56 | * @param DOMElement $parentNode 57 | * @param array $lines 58 | * @param array $request 59 | * @param bool $needOneLineOnly 60 | * @return DOMElement|null 61 | * @throws Exception 62 | */ 63 | public static function checkAppend( 64 | DOMElement $parentNode, 65 | array &$lines, 66 | array $request, 67 | bool $needOneLineOnly = false 68 | ): ?DOMElement { 69 | $man = Man::instance(); 70 | 71 | if ($needOneLineOnly && $parentNode->tagName === 'dt') { // See e.g. links2.1 72 | if (count($request['arguments'])) { 73 | $lines[0] = $request['arguments'][0]; 74 | } else { 75 | array_shift($lines); 76 | } 77 | $man->resetFonts(); 78 | 79 | return null; 80 | } 81 | 82 | array_shift($lines); 83 | 84 | $dom = $parentNode->ownerDocument; 85 | 86 | $parentNode = Blocks::getBlockContainerParent($parentNode); 87 | 88 | if (count($request['arguments']) > 1) { 89 | $indentVal = Roff\Unit::normalize($request['arguments'][1], 'n', 'n'); 90 | if (is_numeric($indentVal)) { 91 | $man->indentation = $indentVal; 92 | } else { 93 | $indentVal = $man->indentation; 94 | } 95 | } else { 96 | $indentVal = $man->indentation; 97 | } 98 | 99 | if (count($request['arguments']) > 0) { 100 | $dt = $dom->createElement('dt'); 101 | TextContent::interpretAndAppendText($dt, $request['arguments'][0]); 102 | } 103 | 104 | // Could have hit double quotes, null, \& (zero-width space), just font settings... 105 | if (isset($dt) && \Manner\Text::trimAndRemoveZWSUTF8($dt->textContent) !== '') { 106 | $dl = DefinitionList::getParentDL($parentNode); 107 | 108 | $dd = $dom->createElement('dd'); 109 | 110 | // TODO: See about adding a check like $dl->lastChild->getAttribute('indent') <= $indentVal 111 | // And reducing indent if $indentVal is greater 112 | // And creating new $dl if $indentVal is less 113 | 114 | if (is_null($dl)) { 115 | $dl = $dom->createElement('dl'); 116 | $dl = $parentNode->appendChild($dl); 117 | } 118 | 119 | $dl->appendChild($dt); 120 | 121 | $man->resetFonts(); 122 | 123 | Indentation::set($dd, $indentVal); 124 | $dd = $dl->appendChild($dd); 125 | 126 | /* @var DomElement $dd */ 127 | return $dd; 128 | } else { 129 | $man->resetFonts(); 130 | 131 | 132 | $div = $dom->createElement('div'); 133 | $div->setAttribute('remap', 'IP'); 134 | if (!$indentVal) { 135 | // Resetting indentation, exit dd 136 | $parentNode = Blocks::getBlockContainerParent($parentNode, true); 137 | } elseif ($parentNode->tagName !== 'dd' || Indentation::get($parentNode) !== (float)$indentVal) { 138 | Indentation::set($div, $indentVal); 139 | } 140 | /* @var DomElement $div */ 141 | $div = $parentNode->appendChild($div); 142 | 143 | return $div; 144 | } 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /lib/Block/P.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use Exception; 26 | use Manner\Blocks; 27 | use Manner\Man; 28 | use Manner\Node; 29 | 30 | class P implements Template 31 | { 32 | 33 | /** 34 | * @param DOMElement $parentNode 35 | * @param array $lines 36 | * @param array $request 37 | * @param bool $needOneLineOnly 38 | * @return DOMElement|null 39 | * @throws Exception 40 | */ 41 | public static function checkAppend( 42 | DOMElement $parentNode, 43 | array &$lines, 44 | array $request, 45 | bool $needOneLineOnly = false 46 | ): ?DOMElement { 47 | // This could improve slightly the output of readlink.1 but not worth it as doesn't change any other files, 48 | // and .HP has been deprecated. 49 | // if ( 50 | // $request['request'] === 'HP' 51 | // && count($request['arguments']) === 0 52 | // && count($lines) > 2 53 | // && $lines[2] === '.TP' 54 | // ) { 55 | // $lines[0] = '.TP'; 56 | // array_splice($lines, 2, 0, '[empty]'); 57 | // 58 | // return null; 59 | // } 60 | 61 | 62 | array_shift($lines); 63 | 64 | $man = Man::instance(); 65 | $man->resetIndentationToDefault(); 66 | $man->resetFonts(); 67 | 68 | if ($parentNode->tagName === 'p' && !Node::hasContent($parentNode)) { 69 | return null; // Use existing parent node for content that will follow. 70 | } else { 71 | $parentNode = Blocks::getBlockContainerParent($parentNode); 72 | if ($parentNode->tagName === 'dd') { 73 | $parentNode = $parentNode->parentNode->parentNode; 74 | } 75 | 76 | $p = $parentNode->ownerDocument->createElement('p'); 77 | /* @var DomElement $p */ 78 | $p = $parentNode->appendChild($p); 79 | 80 | return $p; 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /lib/Block/Preformatted.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | // TODO: handle this differently? Use a flag for preformat? 25 | // See esp. submit.1: 26 | /* 27 | * environment variable \fBSGE_TASK_ID\fP. The option arguments n, m and s will be available through the environment variables \fBSGE_TASK_FIRST\fP, \fBSGE_TASK_LAST\fP and \fBSGE_TASK_STEPSIZE\fP. 28 | .sp 1 29 | .nf 30 | .RS 31 | Following restrictions apply to the values n and m: 32 | .sp 1 33 | .RS 34 | 1 <= n <= MIN(2^31-1, max_aj_tasks) 35 | 1 <= m <= MIN(2^31-1, max_aj_tasks) 36 | n <= m 37 | .RE 38 | .fi 39 | .sp 1 40 | \fImax_aj_tasks\fP is defined in the cluster configuration (see 41 | .M sge_conf 5) 42 | .sp 1 43 | The task id range 44 | * 45 | */ 46 | 47 | use DOMElement; 48 | use Exception; 49 | use Manner\Blocks; 50 | use Manner\Indentation; 51 | use Manner\Man; 52 | use Manner\Node; 53 | use Manner\Request; 54 | 55 | class Preformatted implements Template 56 | { 57 | 58 | /** 59 | * @param DOMElement $parentNode 60 | * @param array $lines 61 | * @param array $request 62 | * @param bool $needOneLineOnly 63 | * @return DOMElement|null 64 | * @throws Exception 65 | */ 66 | public static function checkAppend( 67 | DOMElement $parentNode, 68 | array &$lines, 69 | array $request, 70 | bool $needOneLineOnly = false 71 | ): ?DOMElement { 72 | $man = Man::instance(); 73 | 74 | array_shift($lines); 75 | 76 | if (Node::isOrInTag($parentNode, 'pre') || !count($lines)) { 77 | return null; 78 | } 79 | 80 | $firstInternalLine = Request::peepAt($lines[0]); 81 | 82 | if ($firstInternalLine['name'] === 'PP') { 83 | array_shift($lines); 84 | $parentNode = Blocks::getBlockContainerParent($parentNode, true); 85 | } else { 86 | $parentNode = Blocks::getBlockContainerParent($parentNode, false, true); 87 | } 88 | 89 | $pre = $parentNode->ownerDocument->createElement('pre'); 90 | 91 | if ($firstInternalLine['name'] === 'IP' && $firstInternalLine['raw_arg_string'] === '') { 92 | array_shift($lines); 93 | Indentation::set($pre, $man->indentation); 94 | } 95 | 96 | /* @var DomElement $pre */ 97 | $pre = $parentNode->appendChild($pre); 98 | 99 | return $pre; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /lib/Block/RE.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use Exception; 26 | use Manner\Indentation; 27 | use Manner\Man; 28 | use Manner\Node; 29 | 30 | /** 31 | * .RE [nnn] 32 | * This macro moves the left margin back to level nnn, restoring the previous left margin. If no argument is given, it 33 | * moves one level back. The first level (i.e., no call to .RS yet) has number 1, and each call to .RS increases the 34 | * level by 1. 35 | * 36 | * .de1 RE 37 | * . ie \\n[.$] .nr an-level ((;\\$1) ? \\n[an-level]) 40 | * . nr an-margin \\n[an-saved-margin\\n[an-level]] 41 | * . nr an-prevailing-indent \\n[an-saved-prevailing-indent\\n[an-level]] 42 | * . in \\n[an-margin]u 43 | * .. 44 | * 45 | */ 46 | class RE implements Template 47 | { 48 | 49 | /** 50 | * @throws Exception 51 | */ 52 | public static function checkAppend( 53 | DOMElement $parentNode, 54 | array &$lines, 55 | array $request, 56 | bool $needOneLineOnly = false 57 | ): ?DOMElement { 58 | array_shift($lines); 59 | 60 | $man = Man::instance(); 61 | $anLevel = (int)$man->getRegister('an-level'); 62 | 63 | $backToLevel = $anLevel - 1; // Back to one before last by default. 64 | if (count($request['arguments'])) { 65 | $backToLevel = (int)$request['arguments'][0]; 66 | if ($backToLevel < 2) { 67 | // .RE 1 is back to base level (used e.g. in lsmcli.1). 68 | $man->setRegister('an-level', '1'); 69 | $man->resetIndentationToDefault(); 70 | 71 | return Node::ancestor($parentNode, 'section'); 72 | } 73 | } 74 | 75 | $lastDIV = $parentNode; 76 | 77 | while ($anLevel > $backToLevel) { 78 | --$anLevel; 79 | while ($lastDIV = Node::ancestor($lastDIV, 'div')) { 80 | if ($lastDIV->hasAttribute('remap')) { 81 | $lastDIV = $lastDIV->parentNode; 82 | } else { 83 | break; 84 | } 85 | } 86 | if (is_null($lastDIV)) { 87 | $man->setRegister('an-level', '1'); 88 | $man->resetIndentationToDefault(); 89 | 90 | return Node::ancestor($parentNode, 'section'); 91 | } 92 | } 93 | 94 | $man->setRegister('an-level', (string)$anLevel); 95 | 96 | // Restore prevailing indent (see macro definition above) 97 | if (Indentation::isSet($lastDIV->parentNode)) { 98 | $man->indentation = (string)Indentation::get($lastDIV->parentNode); 99 | } else { 100 | $man->resetIndentationToDefault(); 101 | } 102 | 103 | return $lastDIV->parentNode; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /lib/Block/RS.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use Exception; 26 | use Manner\Blocks; 27 | use Manner\Man; 28 | use Manner\Roff\Unit; 29 | 30 | /** 31 | * This macro moves the left margin to the right by the value nnn if specified (default unit is ‘n’); otherwise it is 32 | * set to the previous indentation value specified with .TP, .IP, or .HP (or to the default value if none of them have 33 | * been used yet). The indentation value is then set to the default. 34 | * 35 | * Calls to the RS macro can be nested. 36 | * 37 | * .de1 RS 38 | * . nr an-saved-margin\\n[an-level] \\n[an-margin] 39 | * . nr an-saved-prevailing-indent\\n[an-level] \\n[an-prevailing-indent] 40 | * . ie \\n[.$] .nr an-margin +(n;\\$1) 41 | * . el .nr an-margin +\\n[an-prevailing-indent] 42 | * . in \\n[an-margin]u 43 | * . nr an-prevailing-indent \\n[IN] 44 | * . nr an-level +1 45 | * .. 46 | * 47 | */ 48 | class RS implements Template 49 | { 50 | 51 | /** 52 | * @param DOMElement $parentNode 53 | * @param array $lines 54 | * @param array $request 55 | * @param bool $needOneLineOnly 56 | * @return DOMElement|null 57 | * @throws Exception 58 | */ 59 | public static function checkAppend( 60 | DOMElement $parentNode, 61 | array &$lines, 62 | array $request, 63 | bool $needOneLineOnly = false 64 | ): ?DOMElement { 65 | array_shift($lines); 66 | 67 | $dom = $parentNode->ownerDocument; 68 | $man = Man::instance(); 69 | 70 | // See: https://www.mankier.com/1/zip 71 | // $parentNode = Blocks::getBlockContainerParent($parentNode, true); 72 | $parentNode = Blocks::getBlockContainerParent($parentNode); 73 | 74 | if (count($request['arguments'])) { 75 | $leftMargin = Unit::normalize($request['arguments'][0], 'n', 'n'); 76 | } else { 77 | $leftMargin = $man->indentation; 78 | } 79 | 80 | $newAnLevel = (int)$man->getRegister('an-level') + 1; 81 | $man->setRegister('an-level', (string)$newAnLevel); 82 | 83 | $man->resetIndentationToDefault(); 84 | 85 | $div = $dom->createElement('div'); 86 | $div->setAttribute('left-margin', (string)$leftMargin); 87 | /* @var DomElement $div */ 88 | $div = $parentNode->appendChild($div); 89 | 90 | return $div; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /lib/Block/SY.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use DOMText; 26 | use Exception; 27 | use Manner\Blocks; 28 | use Manner\Request; 29 | use Manner\Roff; 30 | use Manner\TextContent; 31 | 32 | class SY implements Template 33 | { 34 | 35 | /** 36 | * @param DOMElement $parentNode 37 | * @param array $lines 38 | * @param array $request 39 | * @param bool $needOneLineOnly 40 | * @return DOMElement|null 41 | * @throws Exception 42 | */ 43 | public static function checkAppend( 44 | DOMElement $parentNode, 45 | array &$lines, 46 | array $request, 47 | bool $needOneLineOnly = false 48 | ): ?DOMElement { 49 | $dom = $parentNode->ownerDocument; 50 | $parentNode = Blocks::getBlockContainerParent($parentNode, true); 51 | 52 | $foundEnd = false; 53 | 54 | $syRows = []; 55 | $syLines = []; 56 | $lastCommandName = ''; 57 | $firstLine = true; 58 | while ($request = Request::getLine($lines)) { 59 | if (in_array($request['request'], ['SH', 'SS'])) { 60 | $foundEnd = true; 61 | // no array_shift(): we need this line for later 62 | $syRows[] = ['cmd_name' => $lastCommandName, 'sy_lines' => $syLines]; 63 | break; 64 | } 65 | array_shift($lines); 66 | if ($request['request'] === 'SY') { 67 | $lastCommandName = count($request['arguments']) ? $request['arguments'][0] : ''; 68 | if (!$firstLine) { // Don't do this on first .SY 69 | $syRows[] = ['cmd_name' => $lastCommandName, 'sy_lines' => $syLines]; 70 | } 71 | $syLines = []; 72 | } elseif ($request['request'] === 'YS') { 73 | $syRows[] = ['cmd_name' => $lastCommandName, 'sy_lines' => $syLines]; 74 | $foundEnd = true; 75 | break; 76 | } else { 77 | $syLines[] = $request['raw_line']; 78 | } 79 | $firstLine = false; 80 | } 81 | 82 | if (!$foundEnd) { 83 | throw new Exception('SY not followed by YS, SH, or SS.'); 84 | } 85 | 86 | $table = $dom->createElement('table'); 87 | $table->setAttribute('class', 'synopsis'); 88 | 89 | foreach ($syRows as $syRow) { 90 | $tr = $table->appendChild($dom->createElement('tr')); 91 | $tdCommandName = $tr->appendChild($dom->createElement('td')); 92 | 93 | if ($syRow['cmd_name'] !== '') { 94 | $syRow['cmd_name'] = mb_trim(TextContent::interpretString($syRow['cmd_name'])); 95 | $tdCommandName->appendChild(new DOMText($syRow['cmd_name'])); 96 | } 97 | 98 | /* @var DomElement $tdOptions */ 99 | $tdOptions = $tr->appendChild($dom->createElement('td')); 100 | 101 | Roff::parse($tdOptions, $syRow['sy_lines']); 102 | } 103 | 104 | $parentNode->appendChild($table); 105 | 106 | return $parentNode; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /lib/Block/Section.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use Exception; 26 | use Manner\Man; 27 | use Manner\Node; 28 | use Manner\Roff; 29 | use Manner\TextContent; 30 | 31 | class Section implements Template 32 | { 33 | 34 | /** 35 | * @param DOMElement $parentNode 36 | * @param array $lines 37 | * @param array $request 38 | * @param bool $needOneLineOnly 39 | * @return DOMElement|null 40 | * @throws Exception 41 | */ 42 | public static function checkAppend( 43 | DOMElement $parentNode, 44 | array &$lines, 45 | array $request, 46 | bool $needOneLineOnly = false 47 | ): ?DOMElement { 48 | array_shift($lines); 49 | 50 | $dom = $parentNode->ownerDocument; 51 | 52 | $man = Man::instance(); 53 | $man->resetIndentationToDefault(); 54 | $man->resetFonts(); 55 | 56 | $body = Node::ancestor($parentNode, 'body'); 57 | $section = $dom->createElement('section'); 58 | /* @var DomElement $headingNode */ 59 | if ($request['request'] === 'SH') { 60 | $section = $body->appendChild($section); 61 | $headingNode = $dom->createElement('h2'); 62 | } else { 63 | if ($body->lastChild && $body->lastChild->tagName === 'section') { 64 | $superSection = $body->lastChild; 65 | } else { 66 | // Make a new h2 level container section: 67 | $superSection = $body->appendChild($dom->createElement('section')); 68 | } 69 | $section = $superSection->appendChild($section); 70 | $headingNode = $dom->createElement('h3'); 71 | } 72 | 73 | /** @var DOMElement $headingNode */ 74 | $headingNode = $section->appendChild($headingNode); 75 | 76 | if (count($request['arguments']) === 0) { 77 | $gotContent = Roff::parse($headingNode, $lines, true); 78 | if (!$gotContent) { 79 | $section->parentNode->removeChild($section); 80 | 81 | return null; 82 | } 83 | } else { 84 | $sectionHeading = implode(' ', $request['arguments']); // .SH "A B" sames as .SH A B 85 | TextContent::interpretAndAppendText($headingNode, $sectionHeading); 86 | if ($headingNode->lastChild) { 87 | // We don't want empty sections with   as heading. See e.g. ntptime.8 88 | $headingNode->lastChild->textContent = mb_rtrim( 89 | $headingNode->lastChild->textContent, 90 | " \t\n\r\0\x0B" . html_entity_decode(' ') 91 | ); 92 | } 93 | // Skip sections with empty headings 94 | if (mb_trim($headingNode->textContent) === '') { 95 | $section->parentNode->removeChild($section); 96 | 97 | return null; 98 | } 99 | } 100 | 101 | return $section; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /lib/Block/TH.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use DOMText; 26 | use Exception; 27 | use Manner\Man; 28 | use Manner\Node; 29 | use Manner\Replace; 30 | use Manner\TextContent; 31 | 32 | class TH implements Template 33 | { 34 | 35 | /** 36 | * @param DOMElement $parentNode 37 | * @param array $lines 38 | * @param array $request 39 | * @param bool $needOneLineOnly 40 | * @return DOMElement|null 41 | * @throws Exception 42 | */ 43 | public static function checkAppend( 44 | DOMElement $parentNode, 45 | array &$lines, 46 | array $request, 47 | bool $needOneLineOnly = false 48 | ): ?DOMElement { 49 | array_shift($lines); 50 | 51 | $man = Man::instance(); 52 | 53 | $body = Node::ancestor($parentNode, 'body'); 54 | 55 | if (empty($man->title)) { 56 | if (count($request['arguments']) < 1) { 57 | throw new Exception($request['raw_line'] . ' - missing title info'); 58 | } 59 | 60 | foreach ($request['arguments'] as $k => $v) { 61 | // See amor.6 for \FB \FR nonsense. 62 | $value = Replace::preg('~\\\\F[BR]~', '', $v); 63 | $value = TextContent::interpretString($value); 64 | // Fix vnu's "Saw U+0000 in stream" e.g. in lvmsadc.8: 65 | $value = mb_trim($value); 66 | $request['arguments'][$k] = $value; 67 | } 68 | 69 | $man->title = $request['arguments'][0]; 70 | if (count($request['arguments']) > 1) { 71 | $man->section = $request['arguments'][1]; 72 | $man->extra1 = @$request['arguments'][2] ?: ''; 73 | $man->extra2 = @$request['arguments'][3] ?: ''; 74 | $man->extra3 = @$request['arguments'][4] ?: ''; 75 | } 76 | 77 | $h1 = $body->ownerDocument->createElement('h1'); 78 | $h1->appendChild(new DOMText($man->title)); 79 | $body->appendChild($h1); 80 | } elseif (count($request['arguments'])) { 81 | // Some pages have multiple .THs for different commands in one page, just had a horizontal line when we hit 82 | // .THs with content after the first 83 | $hr = $body->ownerDocument->createElement('hr'); 84 | $body->appendChild($hr); 85 | } 86 | 87 | return $body; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /lib/Block/TabTable.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use Exception; 26 | use Manner\Request; 27 | use Manner\TextContent; 28 | 29 | /** 30 | * Make tables out of tab-separated lines 31 | */ 32 | class TabTable implements Template 33 | { 34 | 35 | public const array skippableLines = ['.br', '']; 36 | 37 | // \&... see pmlogextract.1 38 | public const array specialAcceptableLines = ['\\&...']; 39 | 40 | private static function isTabTableLine($line): bool 41 | { 42 | $line = mb_trim($line); 43 | 44 | return 45 | mb_strpos($line, "\t") !== false || 46 | in_array($line, self::skippableLines) || 47 | in_array($line, self::specialAcceptableLines); 48 | } 49 | 50 | public static function lineContainsTab(string $line): bool 51 | { 52 | $line = mb_ltrim($line, '\\&'); 53 | 54 | // first char is NOT a tab + non-white-space before tab avoid indented stuff + exclude escaped tabs 55 | return mb_strpos($line, "\t") > 0 && preg_match('~[^\\\\\s]\t~u', $line); 56 | } 57 | 58 | public static function isStart(array $lines): bool 59 | { 60 | return 61 | count($lines) > 2 && 62 | !is_null($lines[0]) && !is_null($lines[1]) && 63 | !in_array(mb_substr($lines[0], 0, 1), ['.', '\'']) && 64 | self::lineContainsTab($lines[0]) && 65 | ( 66 | self::lineContainsTab($lines[1]) || 67 | in_array(mb_trim($lines[1]), self::skippableLines + self::specialAcceptableLines) 68 | ) && 69 | self::lineContainsTab($lines[2]); 70 | } 71 | 72 | /** 73 | * @param DOMElement $parentNode 74 | * @param array $lines 75 | * @param array $request 76 | * @param bool $needOneLineOnly 77 | * @return DOMElement|null 78 | * @throws Exception 79 | */ 80 | public static function checkAppend( 81 | DOMElement $parentNode, 82 | array &$lines, 83 | array $request, 84 | bool $needOneLineOnly = false 85 | ): ?DOMElement { 86 | $dom = $parentNode->ownerDocument; 87 | 88 | if ($parentNode->tagName === 'p') { 89 | $parentNode = $parentNode->parentNode; 90 | } 91 | 92 | $table = $dom->createElement('table'); 93 | $parentNode->appendChild($table); 94 | 95 | while ($nextRequest = Request::getLine($lines)) { 96 | if (!self::isTabTableLine($nextRequest['raw_line'])) { 97 | break; 98 | } 99 | 100 | array_shift($lines); 101 | 102 | if (in_array(mb_trim($nextRequest['raw_line']), self::skippableLines)) { 103 | continue; 104 | } 105 | 106 | $tds = preg_split('~\t+~u', $nextRequest['raw_line']); 107 | $tr = $table->appendChild($dom->createElement('tr')); 108 | foreach ($tds as $tdLine) { 109 | $cell = $dom->createElement('td'); 110 | TextContent::interpretAndAppendText($cell, $tdLine); 111 | $tr->appendChild($cell); 112 | } 113 | } 114 | 115 | return $parentNode; 116 | } 117 | 118 | 119 | } 120 | -------------------------------------------------------------------------------- /lib/Block/Template.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | 26 | interface Template 27 | { 28 | 29 | /** 30 | * @param DOMElement $parentNode 31 | * @param array $lines 32 | * @param array $request 33 | * @param bool $needOneLineOnly 34 | * @return DOMElement|null The new parent node to use for following elements, or null we don't want to change that. 35 | */ 36 | public static function checkAppend( 37 | DOMElement $parentNode, 38 | array &$lines, 39 | array $request, 40 | bool $needOneLineOnly = false 41 | ): ?DOMElement; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /lib/Block/ad.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use Manner\PreformattedOutput; 26 | 27 | class ad implements Template 28 | { 29 | 30 | public static function checkAppend( 31 | DOMElement $parentNode, 32 | array &$lines, 33 | array $request, 34 | bool $needOneLineOnly = false 35 | ): ?DOMElement { 36 | array_shift($lines); 37 | 38 | if (in_array($request['arg_string'], ['', 'n', 'b']) && $parentNode->tagName === 'pre') { 39 | PreformattedOutput::reset(); 40 | 41 | return $parentNode->parentNode; 42 | } else { 43 | return null; 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /lib/Block/ce.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use Exception; 26 | use Manner\Blocks; 27 | use Manner\Request; 28 | use Manner\Roff; 29 | 30 | class ce implements Template 31 | { 32 | 33 | /** 34 | * @param DOMElement $parentNode 35 | * @param array $lines 36 | * @param array $request 37 | * @param bool $needOneLineOnly 38 | * @return DOMElement|null 39 | * @throws Exception 40 | */ 41 | public static function checkAppend( 42 | DOMElement $parentNode, 43 | array &$lines, 44 | array $request, 45 | bool $needOneLineOnly = false 46 | ): ?DOMElement { 47 | array_shift($lines); 48 | $parentNode = Blocks::getBlockContainerParent($parentNode); 49 | $dom = $parentNode->ownerDocument; 50 | $block = $dom->createElement('p'); 51 | $block->setAttribute('class', 'center'); 52 | $parentNode->appendChild($block); 53 | 54 | $numLinesToCenter = count($request['arguments']) === 0 ? 1 : (int)$request['arguments'][0]; 55 | $centerLinesUpTo = min($numLinesToCenter, count($lines)); 56 | for ($i = 0; $i < $centerLinesUpTo && count($lines); ++$i) { 57 | if (Request::getLine($lines)['request'] === 'ce') { 58 | break; 59 | } 60 | Roff::parse($block, $lines, true); 61 | $block->appendChild($dom->createElement('br')); 62 | } 63 | 64 | return $parentNode; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /lib/Block/fc.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMDocument; 25 | use DOMElement; 26 | use DOMException; 27 | use Exception; 28 | use Manner\Blocks; 29 | use Manner\Request; 30 | use Manner\TextContent; 31 | 32 | class fc implements Template 33 | { 34 | 35 | /** 36 | * @throws DOMException 37 | */ 38 | private static function addRow(DOMDocument $dom, DOMElement $table, array $cells): void 39 | { 40 | $tr = $dom->createElement('tr'); 41 | foreach ($cells as $contents) { 42 | $td = $dom->createElement('td'); 43 | TextContent::interpretAndAppendText($td, $contents); 44 | $tr->appendChild($td); 45 | } 46 | $table->appendChild($tr); 47 | } 48 | 49 | /** 50 | * @param DOMElement $parentNode 51 | * @param array $lines 52 | * @param array $request 53 | * @param bool $needOneLineOnly 54 | * @return DOMElement|null 55 | * @throws Exception 56 | */ 57 | public static function checkAppend( 58 | DOMElement $parentNode, 59 | array &$lines, 60 | array $request, 61 | bool $needOneLineOnly = false 62 | ): ?DOMElement { 63 | array_shift($lines); 64 | 65 | $parentNode = Blocks::getBlockContainerParent($parentNode); 66 | 67 | $delim = $request['arguments'][0]; 68 | $pad = @$request['arguments'][1] ?: ' '; 69 | 70 | $dom = $parentNode->ownerDocument; 71 | 72 | $table = $dom->createElement('table'); 73 | 74 | // We don't want to handle the lines at this stage as a fresh call to .fc call a new \Roff\fc, so don't iterate 75 | // with Request::getLine() 76 | while (count($lines)) { 77 | // Don't process next line yet, could be new .fc 78 | $requestDetails = Request::peepAt($lines[0]); 79 | 80 | if ( 81 | $requestDetails['name'] === 'fi' || 82 | ($requestDetails['name'] === 'fc' && $requestDetails['raw_arg_string'] === '') 83 | ) { 84 | array_shift($lines); 85 | break; // Finished 86 | } 87 | 88 | $nextRequest = Request::getLine($lines); 89 | array_shift($lines); 90 | 91 | if (in_array($nextRequest['request'], ['ta', 'nf', 'br', 'LP'])) { 92 | continue; // Ignore 93 | } elseif (mb_strpos($nextRequest['raw_line'], $delim) === 0) { 94 | $cells = preg_split('~' . preg_quote($delim, '~') . '~u', $nextRequest['raw_line']); 95 | array_shift($cells); 96 | $cells = array_map( 97 | function ($contents) use ($pad) { 98 | return mb_trim($contents, $pad); 99 | }, 100 | $cells 101 | ); 102 | self::addRow($dom, $table, $cells); 103 | } else { 104 | $cells = preg_split("~\t~u", $nextRequest['raw_line']); 105 | self::addRow($dom, $table, $cells); 106 | } 107 | } 108 | 109 | $parentNode->appendChild($table); 110 | 111 | return $parentNode; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /lib/Block/ti.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Block; 23 | 24 | use DOMElement; 25 | use Exception; 26 | use Manner\Blocks; 27 | use Manner\Indentation; 28 | use Manner\Inline\VerticalSpace; 29 | use Manner\Node; 30 | use Manner\Roff\Unit; 31 | 32 | /** 33 | * .ti ±N: Temporary indent next line (default scaling indicator m). 34 | */ 35 | class ti implements Template 36 | { 37 | 38 | /** 39 | * @param DOMElement $parentNode 40 | * @param array $lines 41 | * @param array $request 42 | * @param bool $needOneLineOnly 43 | * @return DOMElement|null 44 | * @throws Exception 45 | */ 46 | public static function checkAppend( 47 | DOMElement $parentNode, 48 | array &$lines, 49 | array $request, 50 | bool $needOneLineOnly = false 51 | ): ?DOMElement { 52 | array_shift($lines); 53 | 54 | $indentVal = 0.0; 55 | if (count($request['arguments'])) { 56 | $indentVal = Unit::normalize($request['arguments'][0], 'm', 'n'); 57 | } 58 | 59 | if (Indentation::get($parentNode) === (float)$indentVal && $parentNode->lastChild) { 60 | if ($parentNode->lastChild->nodeType !== XML_ELEMENT_NODE || $parentNode->lastChild->tagName !== 'br') { 61 | VerticalSpace::addBR($parentNode); 62 | } 63 | 64 | return $parentNode; 65 | } 66 | 67 | $dt = Node::ancestor($parentNode, 'dt'); 68 | if (is_null($dt)) { 69 | $parentNode = Blocks::getBlockContainerParent($parentNode); 70 | $p = $parentNode->ownerDocument->createElement('p'); 71 | /* @var DomElement $p */ 72 | $p = $parentNode->appendChild($p); 73 | Indentation::set($p, $indentVal); 74 | 75 | return $p; 76 | } else { 77 | Indentation::set($dt, $indentVal); 78 | 79 | return null; 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /lib/Blocks.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner; 23 | 24 | use DOMElement; 25 | use DOMException; 26 | use Exception; 27 | 28 | class Blocks 29 | { 30 | 31 | public const array BLOCK_ELEMENTS = [ 32 | 'p', 33 | 'pre', 34 | 'div', 35 | 'dl', 36 | 'ol', 37 | 'ul', 38 | 'table', 39 | ]; 40 | 41 | public const array INLINE_ELEMENTS = [ 42 | 'a', 43 | 'em', 44 | 'strong', 45 | 'small', 46 | 'code', 47 | 'span', 48 | 'sub', 49 | 'sup', 50 | ]; 51 | 52 | /** 53 | * @throws DOMException 54 | */ 55 | public static function getParentForText(DOMElement $parentNode): DOMElement 56 | { 57 | if (in_array($parentNode->tagName, ['body', 'section', 'div', 'dd'])) { 58 | $p = $parentNode->ownerDocument->createElement('p'); 59 | $p->setAttribute('implicit', '1'); 60 | $parentNode = $parentNode->appendChild($p); 61 | } 62 | 63 | return $parentNode; 64 | } 65 | 66 | /** 67 | * @param DOMElement $parentNode 68 | * @param bool $superOnly 69 | * @param bool $ipOK 70 | * @return DOMElement 71 | * @throws Exception 72 | */ 73 | public static function getBlockContainerParent( 74 | DOMElement $parentNode, 75 | bool $superOnly = false, 76 | bool $ipOK = false 77 | ): DOMElement { 78 | $blockTags = ['body', 'div', 'section', 'pre']; 79 | if (!$superOnly) { 80 | $blockTags[] = 'dt'; 81 | $blockTags[] = 'dd'; 82 | $blockTags[] = 'th'; 83 | $blockTags[] = 'td'; 84 | } 85 | 86 | // We use
s to "remap" .IP temporarily so it can contain other
s, so treat remaps as non-blocks in 87 | // some cases. 88 | while (!in_array($parentNode->tagName, $blockTags) || (!$ipOK && $parentNode->hasAttribute('remap'))) { 89 | $parentNode = $parentNode->parentNode; 90 | if (!$parentNode) { 91 | throw new Exception('No more parents.'); 92 | } 93 | } 94 | 95 | return $parentNode; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /lib/Indentation.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner; 23 | 24 | use DOMElement; 25 | use Exception; 26 | 27 | class Indentation 28 | { 29 | 30 | // The default indentation is 7.2n in troff mode and 7n in nroff mode except for grohtml, which ignores indentation. 31 | // (https://www.mankier.com/7/groff_man#Miscellaneous) 32 | public const string DEFAULT = '7'; 33 | 34 | public static function isSet(DOMElement $p): bool 35 | { 36 | return $p->hasAttribute('indent'); 37 | } 38 | 39 | public static function get(DOMElement $el): float 40 | { 41 | return (float)$el->getAttribute('indent'); 42 | } 43 | 44 | public static function isSame(DOMElement $elA, DOMElement $elB): bool 45 | { 46 | return self::get($elA) === self::get($elB); 47 | } 48 | 49 | /** 50 | * @param DOMElement $el 51 | * @param $indentVal 52 | * @throws Exception 53 | */ 54 | public static function set(DOMElement $el, $indentVal): void 55 | { 56 | if (!is_numeric($indentVal)) { 57 | throw new Exception('Non-numeric indent: ' . $indentVal); 58 | } 59 | $el->setAttribute('indent', (string)$indentVal); 60 | } 61 | 62 | public static function remove(DOMElement $el): void 63 | { 64 | $el->removeAttribute('indent'); 65 | } 66 | 67 | /** 68 | * @param DOMElement $el 69 | * @param $indentVal 70 | * @throws Exception 71 | */ 72 | public static function add(DOMElement $el, $indentVal): void 73 | { 74 | if (!is_numeric($indentVal)) { 75 | throw new Exception('Non-numeric indent: ' . $indentVal); 76 | } 77 | self::set($el, self::get($el) + $indentVal); 78 | } 79 | 80 | /** 81 | * @param DOMElement $el 82 | * @param $indentVal 83 | * @throws Exception 84 | */ 85 | public static function subtract(DOMElement $el, $indentVal): void 86 | { 87 | if (!is_numeric($indentVal)) { 88 | throw new Exception('Non-numeric indent: ' . $indentVal); 89 | } 90 | self::set($el, self::get($el) - $indentVal); 91 | } 92 | 93 | /** 94 | * @param DOMElement $remainingNode 95 | * @param DOMElement $leavingNode 96 | * @throws Exception 97 | */ 98 | public static function addElIndent(DOMElement $remainingNode, DOMElement $leavingNode): void 99 | { 100 | $remainingNodeIndent = self::get($remainingNode); 101 | $leavingNodeIndent = self::get($leavingNode); 102 | 103 | if ($leavingNodeIndent) { 104 | if (!$remainingNodeIndent) { 105 | self::set($remainingNode, $leavingNodeIndent); 106 | } else { 107 | self::set($remainingNode, $remainingNodeIndent + $leavingNodeIndent); 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * @param DOMElement $el 114 | * @throws Exception 115 | */ 116 | public static function popOut(DOMElement $el): void 117 | { 118 | $elParent = $el->parentNode; 119 | 120 | // li: see cpupower-monitor.1 121 | if ($elParent->tagName === 'section' || $elParent->tagName === 'li') { 122 | return; 123 | } 124 | 125 | $parentIndent = Indentation::get($elParent); 126 | $inDD = DOM::isTag($elParent, 'dd'); 127 | 128 | if ( 129 | $el !== $elParent->firstChild && 130 | (($inDD && !$elParent->nextSibling) || !$el->nextSibling) && 131 | $parentIndent !== .0 && 132 | $parentIndent <= -Indentation::get($el) 133 | ) { 134 | Indentation::add($el, $parentIndent); 135 | 136 | if ($inDD) { 137 | $el = $elParent->parentNode->parentNode->insertBefore($el, $elParent->parentNode->nextSibling); 138 | } else { 139 | $el = $elParent->parentNode->insertBefore($el, $elParent->nextSibling); 140 | } 141 | /* @var DomElement $el */ 142 | self::popOut($el); 143 | } 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /lib/Inline/AlternatingFont.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Inline; 23 | 24 | use DOMElement; 25 | use Exception; 26 | use Manner\Block\Template; 27 | use Manner\Block\Text; 28 | use Manner\Blocks; 29 | use Manner\Man; 30 | use Manner\Request; 31 | use Manner\TextContent; 32 | 33 | class AlternatingFont implements Template 34 | { 35 | 36 | /** 37 | * @param DOMElement $parentNode 38 | * @param array $lines 39 | * @param array $request 40 | * @param bool $needOneLineOnly 41 | * @return DOMElement|null 42 | * @throws Exception 43 | */ 44 | public static function checkAppend( 45 | DOMElement $parentNode, 46 | array &$lines, 47 | array $request, 48 | bool $needOneLineOnly = false 49 | ): ?DOMElement { 50 | array_shift($lines); 51 | $parentNode = Blocks::getParentForText($parentNode); 52 | $man = Man::instance(); 53 | Text::addSpace($parentNode); 54 | 55 | foreach ($request['arguments'] as $bi => $bit) { 56 | $requestCharIndex = $bi % 2; 57 | if (!isset($request['request'][$requestCharIndex])) { 58 | throw new Exception( 59 | $lines[0] . ' command ' . $request['request'] . ' has nothing at index ' . $requestCharIndex 60 | ); 61 | } 62 | // Re-massage the line: 63 | // in a man page the AlternatingFont macro argument would become the macro argument to a .ft call and have 64 | // double backslashes transformed twice (I think) 65 | $bit = Request::massageLine($bit); 66 | $man->pushFont($request['request'][$requestCharIndex]); 67 | TextContent::interpretAndAppendText($parentNode, $bit); 68 | $man->resetFonts(); 69 | } 70 | 71 | return $parentNode; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /lib/Inline/EQ.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Inline; 23 | 24 | use DOMDocument; 25 | use DOMElement; 26 | use DOMXPath; 27 | use Exception; 28 | use Manner\Block\Template; 29 | use Manner\Block\Text; 30 | use Manner\Man; 31 | use Manner\Node; 32 | use Manner\Request; 33 | 34 | class EQ implements Template 35 | { 36 | 37 | /** 38 | * @param DOMElement $parentNode 39 | * @param array $lines 40 | * @param array $request 41 | * @param bool $needOneLineOnly 42 | * @return DOMElement|null 43 | * @throws Exception 44 | */ 45 | public static function checkAppend( 46 | DOMElement $parentNode, 47 | array &$lines, 48 | array $request, 49 | bool $needOneLineOnly = false 50 | ): ?DOMElement { 51 | array_shift($lines); 52 | 53 | $foundEnd = false; 54 | 55 | $eqLines = []; 56 | while ($request = Request::getLine($lines)) { 57 | array_shift($lines); 58 | if ($request['request'] === 'EN') { 59 | $foundEnd = true; 60 | break; 61 | } else { 62 | $eqLines[] = $request['raw_line']; 63 | } 64 | } 65 | 66 | if (!$foundEnd) { 67 | throw new Exception('EQ without EN.'); 68 | } 69 | 70 | $man = Man::instance(); 71 | 72 | if (count($eqLines) === 1) { 73 | if (preg_match('~^delim (.)(.)$~ui', $eqLines[0], $matches)) { 74 | $man->eq_delim_left = $matches[1]; 75 | $man->eq_delim_right = $matches[2]; 76 | 77 | return null; 78 | } 79 | } 80 | 81 | foreach ($eqLines as $k => $eqLine) { 82 | if ($eqLine === 'delim off') { 83 | $man->eq_delim_left = null; 84 | $man->eq_delim_right = null; 85 | unset($eqLines[$k]); 86 | } 87 | } 88 | 89 | if (count($eqLines) > 0) { 90 | Text::addSpace($parentNode); 91 | self::appendMath($parentNode, $eqLines); 92 | } 93 | 94 | return null; 95 | } 96 | 97 | /** 98 | * @throws Exception 99 | */ 100 | public static function appendMath(DOMElement $parentNode, array $lines): void 101 | { 102 | $eqnString = '.EQ' . PHP_EOL; 103 | foreach ($lines as $line) { 104 | // Hack for mm2gv: 105 | $line = str_replace(['\\fI', '\\fP'], '', $line); 106 | $eqnString .= $line . PHP_EOL; 107 | } 108 | $eqnString .= '.EN' . PHP_EOL; 109 | $tmpFileName = tempnam('/tmp', 'eqn'); 110 | file_put_contents($tmpFileName, $eqnString); 111 | exec('eqn -T MathML ' . $tmpFileName, $output, $returnVar); 112 | if ($returnVar !== 0) { 113 | throw new Exception('Failed to render .EQ content'); 114 | } 115 | // Now get rid of .EQ: 116 | array_shift($output); 117 | // ... and .EN: 118 | array_pop($output); 119 | 120 | $mathString = implode('', $output); 121 | 122 | # Hacks: 123 | $mathString = str_replace([' ', '=', '+'], [' ', '=', '+'], $mathString); 124 | 125 | $mathDoc = new DOMDocument(); 126 | @$mathDoc->loadHTML($mathString); 127 | $mathNode = $mathDoc->getElementsByTagName('math')->item(0); 128 | // $mathNode->setAttribute('xmlns', 'http://www.w3.org/1998/Math/MathML'); 129 | // $mathNode->setAttribute('display', 'inline'); 130 | $xpath = new DOMXpath($mathDoc); 131 | $mErrors = $xpath->query('//merror'); 132 | foreach ($mErrors as $mError) { 133 | Node::remove($mError, false); 134 | } 135 | 136 | $mathNode = $parentNode->ownerDocument->importNode($mathNode, true); 137 | $parentNode->appendChild($mathNode); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /lib/Inline/FontOneInputLine.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Inline; 23 | 24 | use DOMElement; 25 | use DOMException; 26 | use Manner\Block\Template; 27 | use Manner\Block\Text; 28 | use Manner\Blocks; 29 | use Manner\Man; 30 | use Manner\Node; 31 | use Manner\PreformattedOutput; 32 | use Manner\TextContent; 33 | 34 | class FontOneInputLine implements Template 35 | { 36 | 37 | /** 38 | * @throws DOMException 39 | */ 40 | public static function checkAppend( 41 | DOMElement $parentNode, 42 | array &$lines, 43 | array $request, 44 | bool $needOneLineOnly = false 45 | ): ?DOMElement { 46 | array_shift($lines); 47 | 48 | $man = Man::instance(); 49 | 50 | $man->pushFont($request['request']); 51 | 52 | if (count($request['arguments']) === 0) { 53 | $man->addPostOutputCallback( 54 | function () use ($parentNode) { 55 | Man::instance()->resetFonts(); 56 | 57 | return null; 58 | } 59 | ); 60 | 61 | return null; 62 | } else { 63 | $parentNode = Blocks::getParentForText($parentNode); 64 | Text::addSpace($parentNode); 65 | TextContent::interpretAndAppendText($parentNode, implode(' ', $request['arguments'])); 66 | if ($pre = Node::ancestor($parentNode, 'pre')) { 67 | PreformattedOutput::endInputLine($pre); 68 | } 69 | $man->resetFonts(); 70 | 71 | return $parentNode; 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /lib/Inline/Link.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Inline; 23 | 24 | use DOMElement; 25 | use DOMException; 26 | use Manner\Block\Template; 27 | use Manner\Block\Text; 28 | use Manner\Blocks; 29 | use Manner\Node; 30 | use Manner\Replace; 31 | use Manner\TextContent; 32 | 33 | class Link implements Template 34 | { 35 | 36 | /** 37 | * @throws DOMException 38 | */ 39 | public static function checkAppend( 40 | DOMElement $parentNode, 41 | array &$lines, 42 | array $request, 43 | bool $needOneLineOnly = false 44 | ): ?DOMElement { 45 | array_shift($lines); 46 | 47 | $existingAnchor = Node::ancestor($parentNode, 'a'); 48 | 49 | $dom = $parentNode->ownerDocument; 50 | 51 | if (is_null($existingAnchor)) { 52 | $parentNode = Blocks::getParentForText($parentNode); 53 | } else { 54 | $parentNode = $existingAnchor->parentNode; 55 | } 56 | 57 | $anchor = $dom->createElement('a'); 58 | 59 | if (count($request['arguments'])) { 60 | $url = $request['arguments'][0]; 61 | $href = self::getValidHREF($url); 62 | if ($href) { 63 | $anchor->setAttribute('href', $href); 64 | } 65 | } 66 | 67 | Text::addSpace($parentNode); 68 | $parentNode->appendChild($anchor); 69 | 70 | return $anchor; 71 | } 72 | 73 | public static function getValidHREF(string $url): false|string 74 | { 75 | $url = Replace::preg('~^<(.*)>$~u', '$1', $url); 76 | $href = TextContent::interpretString($url); 77 | if (filter_var($href, FILTER_VALIDATE_URL)) { 78 | return $href; 79 | } elseif (filter_var($href, FILTER_VALIDATE_EMAIL)) { 80 | list($user, $server) = explode('@', $href); 81 | 82 | return 'mailto:' . rawurlencode($user) . '@' . rawurlencode($server); 83 | } else { 84 | return false; 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /lib/Inline/LinkEnd.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Inline; 23 | 24 | use DOMElement; 25 | use DOMText; 26 | use Manner\Block\Template; 27 | use Manner\Node; 28 | 29 | class LinkEnd implements Template 30 | { 31 | 32 | public static function checkAppend( 33 | DOMElement $parentNode, 34 | array &$lines, 35 | array $request, 36 | bool $needOneLineOnly = false 37 | ): ?DOMElement { 38 | array_shift($lines); 39 | $anchorNode = Node::ancestor($parentNode, 'a'); 40 | if (is_null($anchorNode)) { 41 | return null; 42 | } 43 | $parentNode = $anchorNode->parentNode; 44 | $punctuation = mb_trim($request['arg_string']); 45 | 46 | $removed = false; 47 | 48 | if ($anchorNode->getAttribute('href') === '') { 49 | $href = Link::getValidHREF($anchorNode->textContent); 50 | if ($href) { 51 | $anchorNode->setAttribute('href', $href); 52 | } else { 53 | Node::remove($anchorNode); 54 | $removed = true; 55 | } 56 | } 57 | 58 | if (!$removed) { 59 | if ($anchorNode->textContent === '') { 60 | $urlAsText = $anchorNode->getAttribute('href'); 61 | $urlAsText = preg_replace('~^mailto:~', '', $urlAsText); 62 | $anchorNode->appendChild(new DOMText($urlAsText)); 63 | } 64 | } 65 | 66 | if ($punctuation !== '') { 67 | $parentNode->appendChild(new DOMText($punctuation)); 68 | } 69 | 70 | return $parentNode; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /lib/Inline/MR.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Inline; 23 | 24 | use DOMElement; 25 | use DOMException; 26 | use DOMText; 27 | use Manner\Block\Template; 28 | use Manner\Block\Text; 29 | use Manner\Blocks; 30 | use Manner\TextContent; 31 | 32 | /** 33 | * https://www.mankier.com/7/groff_man#Description-Hyperlink_macros 34 | */ 35 | class MR implements Template 36 | { 37 | 38 | /** 39 | * @throws DOMException 40 | */ 41 | public static function checkAppend( 42 | DOMElement $parentNode, 43 | array &$lines, 44 | array $request, 45 | bool $needOneLineOnly = false 46 | ): ?DOMElement { 47 | array_shift($lines); 48 | 49 | $dom = $parentNode->ownerDocument; 50 | 51 | $parentNode = Blocks::getParentForText($parentNode); 52 | 53 | $anchor = $dom->createElement('a'); 54 | Text::addSpace($parentNode); 55 | $parentNode->appendChild($anchor); 56 | 57 | $topic = TextContent::interpretString($request['arguments'][0]); 58 | $manualSection = TextContent::interpretString($request['arguments'][1]); 59 | 60 | $anchor->appendChild(new DOMText($topic . '(' . $manualSection . ')')); 61 | $anchor->setAttribute('href', '/' . $manualSection . '/' . $topic); 62 | 63 | // Trailing text (usually punctuation) 64 | if (count($request['arguments']) > 2) { 65 | TextContent::interpretAndAppendText($parentNode, $request['arguments'][2]); 66 | } 67 | 68 | 69 | return $parentNode; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /lib/Inline/OP.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Inline; 23 | 24 | use DOMElement; 25 | use DOMText; 26 | use Exception; 27 | use Manner\Block\Template; 28 | use Manner\Blocks; 29 | use Manner\TextContent; 30 | 31 | /** 32 | * See https://www.mankier.com/7/groff_man#Macros_to_Describe_Command_Synopses 33 | */ 34 | class OP implements Template 35 | { 36 | 37 | /** 38 | * @param DOMElement $parentNode 39 | * @param array $lines 40 | * @param array $request 41 | * @param bool $needOneLineOnly 42 | * @return DOMElement|null 43 | * @throws Exception 44 | */ 45 | public static function checkAppend( 46 | DOMElement $parentNode, 47 | array &$lines, 48 | array $request, 49 | bool $needOneLineOnly = false 50 | ): ?DOMElement { 51 | array_shift($lines); 52 | $dom = $parentNode->ownerDocument; 53 | $parentNode = Blocks::getParentForText($parentNode); 54 | 55 | // Used to prevent line-breaks inside options: 56 | $optSpan = $parentNode->appendChild($dom->createElement('span')); 57 | $optSpan->setAttribute('class', 'opt'); 58 | 59 | $optSpan->appendChild(new DOMText('[')); 60 | /* @var DomElement $strong */ 61 | $strong = $optSpan->appendChild($dom->createElement('strong')); 62 | TextContent::interpretAndAppendText($strong, $request['arguments'][0]); 63 | if (count($request['arguments']) > 1) { 64 | $optSpan->appendChild(new DOMText(' ')); 65 | /* @var DomElement $em */ 66 | $em = $optSpan->appendChild($dom->createElement('em')); 67 | TextContent::interpretAndAppendText($em, $request['arguments'][1]); 68 | } 69 | $optSpan->appendChild(new DOMText('] ')); 70 | 71 | return $parentNode; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /lib/Inline/URL.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Inline; 23 | 24 | use DOMElement; 25 | use DOMText; 26 | use Exception; 27 | use Manner\Block\Template; 28 | use Manner\Block\Text; 29 | use Manner\Blocks; 30 | use Manner\Roff; 31 | use Manner\TextContent; 32 | 33 | class URL implements Template 34 | { 35 | 36 | /** 37 | * @param DOMElement $parentNode 38 | * @param array $lines 39 | * @param array $request 40 | * @param bool $needOneLineOnly 41 | * @return DOMElement|null 42 | * @throws Exception 43 | */ 44 | public static function checkAppend( 45 | DOMElement $parentNode, 46 | array &$lines, 47 | array $request, 48 | bool $needOneLineOnly = false 49 | ): ?DOMElement { 50 | array_shift($lines); 51 | $dom = $parentNode->ownerDocument; 52 | $parentNode = Blocks::getParentForText($parentNode); 53 | 54 | Text::addSpace($parentNode); 55 | if (count($request['arguments']) === 0) { 56 | throw new Exception('Not enough arguments to .URL: ' . $request['raw_line']); 57 | } 58 | 59 | $url = TextContent::interpretString($request['arguments'][0]); 60 | $href = Link::getValidHREF($url); 61 | if ($href) { 62 | $anchor = $dom->createElement('a'); 63 | $anchor->setAttribute('href', $href); 64 | $parentNode->appendChild($anchor); 65 | } else { 66 | $anchor = $dom->createElement('span'); 67 | $parentNode->appendChild($anchor); 68 | } 69 | 70 | $parentNode->appendChild($anchor); 71 | 72 | if (count($request['arguments']) > 1) { 73 | TextContent::interpretAndAppendText($anchor, $request['arguments'][1]); 74 | } elseif (count($lines)) { 75 | Roff::parse($anchor, $lines, true); 76 | } 77 | 78 | if ($anchor->textContent === '') { 79 | $anchor->appendChild(new DOMText($url)); 80 | } 81 | 82 | if (count($request['arguments']) === 3) { 83 | TextContent::interpretAndAppendText($parentNode, $request['arguments'][2]); 84 | } 85 | 86 | return $parentNode; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /lib/Inline/VerticalSpace.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Inline; 23 | 24 | use DOMElement; 25 | use DOMException; 26 | use Manner\Block\Template; 27 | use Manner\Request; 28 | 29 | class VerticalSpace implements Template 30 | { 31 | 32 | /** 33 | * @throws DOMException 34 | */ 35 | public static function addBR(DOMElement $parentNode): void 36 | { 37 | $prevBRs = 0; 38 | $nodeCheck = $parentNode->lastChild; 39 | while ($nodeCheck) { 40 | if ($nodeCheck instanceof DOMElement && $nodeCheck->tagName === 'br') { 41 | ++$prevBRs; 42 | } else { 43 | break; 44 | } 45 | $nodeCheck = $nodeCheck->previousSibling; 46 | } 47 | if ($prevBRs < 2) { 48 | $parentNode->appendChild($parentNode->ownerDocument->createElement('br')); 49 | } 50 | } 51 | 52 | public static function check(string $string): bool 53 | { 54 | return in_array(Request::peepAt($string)['name'], ['br', 'sp', 'ne']); 55 | } 56 | 57 | /** 58 | * @throws DOMException 59 | */ 60 | public static function checkAppend( 61 | DOMElement $parentNode, 62 | array &$lines, 63 | array $request, 64 | bool $needOneLineOnly = false 65 | ): ?DOMElement { 66 | array_shift($lines); 67 | 68 | /*if (count($request['arguments']) && $request['arguments'][0] === '-1') { 69 | if ($parentNode->lastChild instanceof DOMElement && $parentNode->lastChild->tagName === 'br') { 70 | $parentNode->removeChild($parentNode->lastChild); 71 | } 72 | } else*/ 73 | if ( 74 | !($parentNode->lastChild instanceof DOMElement) || 75 | $parentNode->lastChild->tagName !== 'pre' 76 | ) { 77 | self::addBR($parentNode); 78 | if ($request['request'] !== 'br') { 79 | self::addBR($parentNode); 80 | } 81 | } 82 | 83 | return null; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /lib/Inline/ft.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Inline; 23 | 24 | use DOMElement; 25 | use Manner\Block\Template; 26 | use Manner\Man; 27 | 28 | class ft implements Template 29 | { 30 | 31 | public static function checkAppend( 32 | DOMElement $parentNode, 33 | array &$lines, 34 | array $request, 35 | bool $needOneLineOnly = false 36 | ): ?DOMElement { 37 | array_shift($lines); 38 | $man = Man::instance(); 39 | 40 | // Return to previous font. Same as \f[] or \fP. 41 | if (count($request['arguments']) === 0 || $request['arguments'][0] === 'P') { 42 | $man->popFont(); 43 | } else { 44 | $man->pushFont($request['arguments'][0]); 45 | } 46 | 47 | return null; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /lib/Manner.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner; 23 | 24 | use DOMDocument; 25 | use DOMXPath; 26 | use Exception; 27 | 28 | class Manner 29 | { 30 | 31 | /** 32 | * @param array $fileLines 33 | * @return DOMDocument 34 | * @throws Exception 35 | */ 36 | public static function roffToDOM(array $fileLines): DOMDocument 37 | { 38 | $dom = new DOMDocument('1.0', 'utf-8'); 39 | 40 | $manPageContainer = $dom->createElement('body'); 41 | $manPageContainer = $dom->appendChild($manPageContainer); 42 | 43 | $man = Man::instance(); 44 | $man->reset(); 45 | 46 | $strippedLines = Preprocessor::strip($fileLines); 47 | Roff::parse($manPageContainer, $strippedLines); 48 | $xpath = new DOMXpath($dom); 49 | Massage\Body::trimNodesBeforeH1($xpath); 50 | Massage\P::removeEmpty($xpath); 51 | Massage\DL::mergeAdjacentAndConvertLoneDD($xpath); 52 | Massage\Remap::doAll($xpath); 53 | Massage\Indents::recalculate($xpath); 54 | //TODO: add this, then figure out the issues 55 | // Massage\DL::CreateULs($xpath); 56 | Massage\DIV::removeDIVsWithSingleChild($xpath); 57 | DOM::massage($manPageContainer); 58 | Massage\Tidy::doAll($xpath); 59 | Massage\DL::checkPrecedingNodes($xpath); 60 | Massage\Tidy::indentAttributeToClass($xpath); 61 | Massage\Block::coalesceAdjacentChildren($xpath); 62 | Massage\DL::CreateOLs($xpath); 63 | 64 | return $dom; 65 | } 66 | 67 | 68 | /** 69 | * @param array $fileLines 70 | * @param string|null $outputFile 71 | * @param bool $bodyOnly 72 | * @throws Exception 73 | */ 74 | public static function roffToHTML(array $fileLines, ?string $outputFile = null, bool $bodyOnly = false): void 75 | { 76 | $dom = self::roffToDOM($fileLines); 77 | $html = $dom->saveHTML(); 78 | 79 | $man = Man::instance(); 80 | 81 | // Remove \& chars aka zero width space. 82 | $html = str_replace(Text::ZERO_WIDTH_SPACE_HTML, '', $html); 83 | $title = Text::trimAndRemoveZWSUTF8($man->title); 84 | $extra1 = Text::trimAndRemoveZWSUTF8($man->extra1); 85 | $extra2 = Text::trimAndRemoveZWSUTF8($man->extra2); 86 | $extra3 = Text::trimAndRemoveZWSUTF8($man->extra3); 87 | 88 | if (!$title) { 89 | $title = 'UNTITLED'; 90 | } 91 | 92 | if ($bodyOnly) { 93 | echo $html; 94 | 95 | return; 96 | } 97 | 98 | $manPageInfo = '' 102 | . ''; 103 | 104 | if (is_null($outputFile)) { 105 | echo '', 106 | '', 107 | $manPageInfo, 108 | '', htmlspecialchars($title), '', 109 | $html; 110 | } else { 111 | $fp = fopen($outputFile, 'w'); 112 | fwrite($fp, ''); 113 | fwrite($fp, ''); 114 | fwrite($fp, $manPageInfo); 115 | fwrite($fp, '' . htmlspecialchars($title) . ''); 116 | fwrite($fp, $html); 117 | fclose($fp); 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /lib/Massage/Body.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Massage; 23 | 24 | use DOMXPath; 25 | use Exception; 26 | use Manner\DOM; 27 | 28 | class Body 29 | { 30 | 31 | /** 32 | * @param DOMXPath $xpath 33 | * @throws Exception 34 | */ 35 | public static function trimNodesBeforeH1(DOMXPath $xpath): void 36 | { 37 | $bodies = $xpath->query('//body'); 38 | if ($bodies->length !== 1) { 39 | throw new Exception('Found more than one body'); 40 | } 41 | $body = $bodies->item(0); 42 | while ($body->firstChild && !DOM::isTag($body->firstChild, 'h1')) { 43 | $body->removeChild($body->firstChild); 44 | } 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /lib/Massage/DT.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Massage; 23 | 24 | use DOMElement; 25 | use DOMException; 26 | use Manner\DOM; 27 | use Manner\Node; 28 | 29 | class DT 30 | { 31 | 32 | /** 33 | * @throws DOMException 34 | */ 35 | public static function postProcess(DOMElement $dt): void 36 | { 37 | $child = $dt->lastChild; 38 | while ($child) { 39 | if (DOM::isTag($child, 'br')) { 40 | $newDT = $dt->ownerDocument->createElement('dt'); 41 | $newDT = $dt->parentNode->insertBefore($newDT, $dt->nextSibling); 42 | 43 | $nextChild = $child->nextSibling; 44 | $dt->removeChild($child); 45 | while ($nextChild) { 46 | $sib = $nextChild->nextSibling; 47 | $newDT->appendChild($nextChild); 48 | $nextChild = $sib; 49 | } 50 | self::postProcess($dt); 51 | } 52 | $child = $child->previousSibling; 53 | } 54 | } 55 | 56 | public static function tidy(DOMElement $dt): void 57 | { 58 | while ($dt->lastChild && (Node::isTextAndEmpty($dt->lastChild) || DOM::isTag($dt->lastChild, 'br'))) { 59 | $dt->removeChild($dt->lastChild); 60 | } 61 | 62 | if (mb_trim($dt->textContent) === '') { 63 | $dt->parentNode->removeChild($dt); 64 | } 65 | 66 | if (Dom::isTag($dt->firstChild, 'pre')) { 67 | //
s can't go inside 
s (tho we put them there for convenience now). 68 | // TODO: remove this once we handle .nf and .EX by setting flag rather than creating
 element.
69 |             Node::remove($dt->firstChild);
70 |             self::tidy($dt);
71 |         }
72 |     }
73 | 
74 | }
75 | 


--------------------------------------------------------------------------------
/lib/Massage/HTMLList.php:
--------------------------------------------------------------------------------
  1 | .
 19 |  */
 20 | declare(strict_types=1);
 21 | 
 22 | namespace Manner\Massage;
 23 | 
 24 | use DOMElement;
 25 | use DOMException;
 26 | use DOMNode;
 27 | use DOMText;
 28 | use Manner\DOM;
 29 | use Manner\Node;
 30 | 
 31 | class HTMLList
 32 | {
 33 | 
 34 |     public const array CHAR_PREFIXES = ['*', 'o', '·', '+', '-', '○'];
 35 | 
 36 |     private static function getBulletRegex(): string
 37 |     {
 38 |         return '~^\s*[' . preg_quote(implode('', HTMLList::CHAR_PREFIXES), '~') . ']\s~u';
 39 |     }
 40 | 
 41 |     public static function startsWithBullet(string $text): bool
 42 |     {
 43 |         return (bool)preg_match(self::getBulletRegex(), $text);
 44 |     }
 45 | 
 46 |     public static function pruneBulletChar(DOMElement $li): void
 47 |     {
 48 |         $firstTextNode = self::getFirstNonEmptyTextNode($li);
 49 |         if ($firstTextNode) {
 50 |             $firstTextNode->textContent = preg_replace(self::getBulletRegex(), '', $firstTextNode->textContent);
 51 |         }
 52 |     }
 53 | 
 54 |     public static function removeLonePs(DOMElement $list): void
 55 |     {
 56 |         $child = $list->firstChild;
 57 |         while ($child) {
 58 |             if ($child->childNodes->length === 1 && DOM::isTag($child->firstChild, 'p')) {
 59 |                 DOM::extractContents($child, $child->firstChild);
 60 |                 $child->removeChild($child->firstChild);
 61 |                 Node::addClass($child, 'p');
 62 |             }
 63 |             $child = $child->nextSibling;
 64 |         }
 65 |     }
 66 | 
 67 |     /**
 68 |      * @throws DOMException
 69 |      */
 70 |     public static function checkElementForLIs(DOMElement $li): bool
 71 |     {
 72 |         $foundInnerLI = false;
 73 | 
 74 |         $child = $li->firstChild;
 75 | 
 76 |         do {
 77 |             if (
 78 |               DOM::isTag($child, 'br') &&
 79 |               $child->nextSibling &&
 80 |               ($child->nextSibling instanceof DOMText || DOM::isInlineElement($child->nextSibling)) &&
 81 |               self::startsWithBullet($child->nextSibling->textContent)
 82 |             ) {
 83 |                 $foundInnerLI = true;
 84 | 
 85 |                 $newLI = $li->ownerDocument->createElement('li');
 86 |                 while ($li->firstChild) {
 87 |                     if ($li->firstChild === $child) {
 88 |                         $li->removeChild($child); // remove the 
89 | break; 90 | } 91 | $newLI->appendChild($li->firstChild); 92 | } 93 | $li->parentNode->insertBefore($newLI, $li); 94 | self::pruneBulletChar($li); 95 | $child = $li->firstChild; 96 | } elseif (DOM::isTag($child, 'p') && self::startsWithBullet($child->textContent)) { 97 | $foundInnerLI = true; 98 | 99 | $newLI = $li->ownerDocument->createElement('li'); 100 | while ($li->firstChild) { 101 | if ($li->firstChild === $child) { 102 | Node::remove($child); // remove the

103 | break; 104 | } 105 | $newLI->appendChild($li->firstChild); 106 | } 107 | $li->parentNode->insertBefore($newLI, $li); 108 | self::pruneBulletChar($li); 109 | $child = $li->firstChild; 110 | } else { 111 | $child = $child->nextSibling; 112 | } 113 | } while ($child); 114 | 115 | return $foundInnerLI; 116 | } 117 | 118 | private static function getFirstNonEmptyTextNode(?DOMNode $domNode): ?DOMText 119 | { 120 | if ($domNode instanceof DOMText) { 121 | if (mb_trim($domNode->textContent) === '') { 122 | $domNode->parentNode->removeChild($domNode); 123 | 124 | return null; 125 | } 126 | 127 | return $domNode; 128 | } 129 | 130 | foreach ($domNode->childNodes as $node) { 131 | if ($el = self::getFirstNonEmptyTextNode($node)) { 132 | return $el; 133 | } 134 | } 135 | 136 | return null; 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /lib/Massage/Indents.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Massage; 23 | 24 | use DOMDocument; 25 | use DOMXPath; 26 | use Exception; 27 | use Manner\DOM; 28 | use Manner\Indentation; 29 | use Manner\Node; 30 | 31 | class Indents 32 | { 33 | 34 | /** 35 | * @param DOMXPath $xpath 36 | * @throws Exception 37 | */ 38 | public static function recalculate(DOMXPath $xpath): void 39 | { 40 | $divs = $xpath->query('//div[@left-margin="0"]'); 41 | foreach ($divs as $div) { 42 | // See tests/warnquota.conf.5 43 | if (DOM::isTag($div->previousSibling, 'p') && DOM::isTag($div->firstChild, 'p')) { 44 | $div->previousSibling->appendChild($div->ownerDocument->createElement('br')); 45 | DOM::extractContents($div->previousSibling, $div->firstChild); 46 | Node::remove($div->firstChild); 47 | } 48 | Node::remove($div); 49 | } 50 | 51 | $divs = $xpath->query('//div[@left-margin]'); 52 | foreach ($divs as $div) { 53 | $leftMargin = (int)$div->getAttribute('left-margin'); 54 | 55 | $parentNode = $div->parentNode; 56 | 57 | while ($parentNode) { 58 | if ($parentNode instanceof DOMDocument || $parentNode->tagName === 'div') { 59 | break; 60 | } 61 | if (Indentation::isSet($parentNode)) { 62 | $leftMargin -= Indentation::get($parentNode); 63 | } 64 | $parentNode = $parentNode->parentNode; 65 | } 66 | Indentation::set($div, $leftMargin); 67 | $div->removeAttribute('left-margin'); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/Massage/LI.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Massage; 23 | 24 | use DOMElement; 25 | use Manner\DOM; 26 | use Manner\Node; 27 | 28 | class LI 29 | { 30 | 31 | public static function tidy(DOMElement $li): void 32 | { 33 | while ($li->lastChild && (Node::isTextAndEmpty($li->lastChild) || DOM::isTag($li->lastChild, 'br'))) { 34 | $li->removeChild($li->lastChild); 35 | } 36 | 37 | if (mb_trim($li->textContent) === '') { 38 | $li->parentNode->removeChild($li); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /lib/Massage/P.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Massage; 23 | 24 | use DOMElement; 25 | use DOMXPath; 26 | use Exception; 27 | use Manner\DOM; 28 | use Manner\Indentation; 29 | use Manner\Node; 30 | 31 | class P 32 | { 33 | 34 | public static function removeEmpty(DOMXPath $xpath): void 35 | { 36 | $ps = $xpath->query('//p'); 37 | foreach ($ps as $p) { 38 | if (!$p->firstChild || mb_trim($p->textContent) === '') { 39 | $p->parentNode->removeChild($p); 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * @param DOMElement $p 46 | * @throws Exception 47 | */ 48 | public static function tidy(DOMElement $p): void 49 | { 50 | // Change two br tags in a row to a new paragraph. 51 | 52 | $indent = Indentation::get($p); 53 | $pChild = $p->firstChild; 54 | 55 | while ($pChild) { 56 | if (DOM::isTag($pChild, 'br') && DOM::isTag($pChild->nextSibling, 'br')) { 57 | $newP = $p->ownerDocument->createElement('p'); 58 | if ($indent) { 59 | Indentation::set($newP, $indent); 60 | } 61 | while ($p->firstChild) { 62 | if ($p->firstChild === $pChild) { 63 | break; 64 | } 65 | $newP->appendChild($p->firstChild); 66 | } 67 | $p->parentNode->insertBefore($newP, $p); 68 | $p->removeChild($p->firstChild); // 1st
69 | $p->removeChild($p->firstChild); // 2nd
70 | self::tidy($p); 71 | self::tidy($newP); 72 | 73 | return; 74 | } 75 | $pChild = $pChild->nextSibling; 76 | } 77 | 78 | while ($p->lastChild && (Node::isTextAndEmpty($p->lastChild) || DOM::isTag($p->lastChild, 'br'))) { 79 | $p->removeChild($p->lastChild); 80 | } 81 | 82 | if (mb_trim($p->textContent) === '') { 83 | $p->parentNode->removeChild($p); 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /lib/Massage/PRE.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Massage; 23 | 24 | use DOMElement; 25 | use DOMText; 26 | use Manner\Node; 27 | use Manner\Text; 28 | 29 | class PRE 30 | { 31 | 32 | public static function tidy(DOMElement $el): void 33 | { 34 | while ($el->lastChild && Node::isTextAndEmpty($el->lastChild)) { 35 | $el->removeChild($el->lastChild); 36 | } 37 | 38 | if (!$el->lastChild) { 39 | $el->parentNode->removeChild($el); 40 | 41 | return; 42 | } 43 | 44 | if ($el->lastChild->nodeType === XML_TEXT_NODE) { 45 | $el->replaceChild(new DOMText(mb_rtrim($el->lastChild->textContent)), $el->lastChild); 46 | } 47 | 48 | if (Text::trimAndRemoveZWSUTF8($el->textContent) === '') { 49 | $el->parentNode->removeChild($el); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /lib/Massage/Remap.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Massage; 23 | 24 | use DOMElement; 25 | use DOMXPath; 26 | use Exception; 27 | use Manner\Blocks; 28 | use Manner\DOM; 29 | use Manner\Indentation; 30 | 31 | class Remap 32 | { 33 | 34 | /** 35 | * @param DOMXPath $xpath 36 | * @throws Exception 37 | */ 38 | public static function doAll(DOMXPath $xpath): void 39 | { 40 | $divs = $xpath->query('//div[@remap]'); 41 | /** @var DOMElement $div */ 42 | /** @var DOMElement $p */ 43 | foreach ($divs as $div) { 44 | if ($div->getAttribute('remap') === 'IP') { 45 | $indentVal = Indentation::get($div); 46 | 47 | $remapChild = $div->firstChild; 48 | if ($remapChild) { 49 | $next = false; 50 | do { 51 | if (DOM::isTag($remapChild, BLOCKS::BLOCK_ELEMENTS)) { 52 | $next = $remapChild->nextSibling; 53 | $remapChild->removeAttribute('implicit'); 54 | $indentVal && Indentation::add($remapChild, $indentVal); 55 | $div->parentNode->insertBefore($remapChild, $div); 56 | } else { 57 | $p = $div->ownerDocument->createElement('p'); 58 | $p = $div->parentNode->insertBefore($p, $div); 59 | $indentVal && Indentation::add($p, $indentVal); 60 | while ($remapChild && !DOM::isTag($remapChild, BLOCKS::BLOCK_ELEMENTS)) { 61 | $next = $remapChild->nextSibling; 62 | $p->appendChild($remapChild); 63 | $remapChild = $next; 64 | } 65 | } 66 | } while ($remapChild = $next); 67 | } 68 | 69 | $div->parentNode->removeChild($div); 70 | } else { 71 | throw new Exception('Unexpected value for remap.'); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/Preprocessor.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner; 23 | 24 | class Preprocessor 25 | { 26 | 27 | public static function strip(array $lines): array 28 | { 29 | $linesNoComments = []; 30 | $linePrefix = ''; 31 | 32 | for ($i = 0; $i < count($lines); ++$i) { 33 | $line = $linePrefix . $lines[$i]; 34 | $linePrefix = ''; 35 | 36 | // Everything up to and including the next newline is ignored. This is interpreted in copy mode. This is like \" except that the terminating newline is ignored as well. 37 | if (preg_match('~(^|.*?[^\\\\])\\\\#~u', $line, $matches)) { 38 | $linePrefix = $matches[1]; 39 | continue; 40 | } 41 | 42 | // Workaround for lots of broken tcl man pages (section n, Tk_*, Tcl_*, others...): 43 | $line = Replace::preg('~^\.\s*el\s?\\\\}~u', '.el \\{', $line); 44 | 45 | // Don't worry about changes in point size for now (see rc.1 for digit instead of +- in \s10): 46 | $line = Replace::preg('~(?. 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner; 23 | 24 | class Replace 25 | { 26 | 27 | public static function preg($pattern, $replacement, string $subject, $limit = -1, &$count = null): array|string 28 | { 29 | $newStr = preg_replace($pattern, $replacement, $subject, $limit, $count); 30 | 31 | if (is_null($newStr)) { 32 | return self::preg($pattern, $replacement, self::ignoreBadChars($subject), $limit, $count); 33 | } 34 | 35 | return $newStr; 36 | } 37 | 38 | public static function pregCallback($pattern, callable $callback, string $subject, $limit = -1, &$count = null) 39 | { 40 | $newStr = preg_replace_callback($pattern, $callback, $subject, $limit, $count); 41 | 42 | if (is_null($newStr)) { 43 | return (self::pregCallback($pattern, $callback, self::ignoreBadChars($subject), $limit, $count)); 44 | } 45 | 46 | return $newStr; 47 | } 48 | 49 | /** 50 | * See https://stackoverflow.com/a/3742879 51 | * @param string $string 52 | * @return string 53 | */ 54 | private static function ignoreBadChars(string $string): string 55 | { 56 | return iconv('UTF-8', 'UTF-8//IGNORE', $string); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /lib/Request/Skippable.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Request; 23 | 24 | use DOMElement; 25 | use Manner\Block\Template; 26 | 27 | class Skippable implements Template 28 | { 29 | 30 | public static function checkAppend( 31 | DOMElement $parentNode, 32 | array &$lines, 33 | array $request, 34 | bool $needOneLineOnly = false 35 | ): ?DOMElement { 36 | array_shift($lines); 37 | 38 | return null; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /lib/Request/Unhandled.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Request; 23 | 24 | use DOMElement; 25 | use Exception; 26 | use Manner\Block\Template; 27 | 28 | class Unhandled implements Template 29 | { 30 | 31 | // Unhandled: 32 | public const array requests = [ 33 | 'ab', 34 | 'aln', 35 | 'am1', 36 | 'ami', 37 | 'ami1', 38 | 'asciify', 39 | 'backtrace', 40 | 'blm', 41 | 'box', 42 | 'boxa', 43 | 'brp', 44 | 'break', 45 | 'c2', 46 | 'cf', 47 | 'ch', 48 | 'chop', 49 | 'class', 50 | 'composite', 51 | 'continue', 52 | 'da', 53 | 'dei', 54 | 'dei1', 55 | 'device', 56 | 'devicem', 57 | 'do', 58 | 'dt', 59 | 'ecr', 60 | 'ecs', 61 | 'fc', 62 | 'fzoom', 63 | 'gcolor', 64 | 'hcode', 65 | 'hla', 66 | 'hlm', 67 | 'hpf', 68 | 'hpfa', 69 | 'hpfcode', 70 | 'kern', 71 | 'lc', // leader repetition glyph. 72 | 'length', 73 | 'linetabs', 74 | 'lg', 75 | 'lsm', 76 | 'ls', 77 | 'ne', 78 | 'nm', 79 | 'nn', 80 | 'nroff', 81 | 'nx', 82 | 'open', 83 | 'opena', 84 | 'os', 85 | 'output', 86 | 'pev', 87 | 'pi', 88 | 'pm', 89 | 'pn', 90 | 'pnr', 91 | 'psbb', 92 | 'pso', 93 | 'ptr', 94 | 'pvs', 95 | 'rd', 96 | 'return', 97 | 'rn', 98 | 'rnn', 99 | 'rr', 100 | 'sizes', 101 | 'special', 102 | 'spreadwarn', 103 | 'sty', 104 | 'substring', 105 | 'sv', 106 | 'sy', 107 | 'tc', 108 | 'tkf', 109 | 'tl', 110 | 'trf', 111 | 'trin', 112 | 'trnt', 113 | 'troff', 114 | 'uf', 115 | 'unformat', 116 | 'vpt', 117 | 'warnscale', 118 | 'write', 119 | 'writec', 120 | 'writem', 121 | ]; 122 | 123 | /** 124 | * @param DOMElement $parentNode 125 | * @param array $lines 126 | * @param array $request 127 | * @param bool $needOneLineOnly 128 | * @return DOMElement|null 129 | * @throws Exception 130 | */ 131 | public static function checkAppend( 132 | DOMElement $parentNode, 133 | array &$lines, 134 | array $request, 135 | bool $needOneLineOnly = false 136 | ): ?DOMElement { 137 | throw new Exception('Unhandled request ' . $request['raw_line']); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /lib/Roff.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner; 23 | 24 | use DOMElement; 25 | use Exception; 26 | 27 | class Roff 28 | { 29 | 30 | /** 31 | * @param DOMElement $parentNode 32 | * @param array $lines 33 | * @param bool $stopOnContent 34 | * @return bool 35 | * @throws Exception 36 | */ 37 | public static function parse( 38 | DOMElement $parentNode, 39 | array &$lines, 40 | bool $stopOnContent = false 41 | ): bool { 42 | while ($request = Request::getLine($lines)) { 43 | if ($stopOnContent) { 44 | // \c: Interrupt text processing (groff.7) 45 | if (in_array($request['raw_line'], ['\\c'])) { 46 | array_shift($lines); 47 | 48 | // Man::instance()->runPostOutputCallbacks(); 49 | return true; 50 | } 51 | 52 | if (in_array($request['request'], ['SH', 'SS', 'TP', 'br', 'sp', 'ne', 'PP', 'RS', 'RE', 'P', 'LP'])) { 53 | return false; 54 | } 55 | 56 | if ($request['raw_line'] === '') { 57 | array_shift($lines); 58 | continue; 59 | } 60 | } 61 | 62 | $request['class'] = Request::getClass($request, $lines); 63 | 64 | if ($newParent = PreformattedOutput::handle($parentNode, $lines, $request)) { 65 | // NB: still need $stopOnContent check below (so no continue) 66 | if ($newParent instanceof DOMElement) { 67 | $parentNode = $newParent; 68 | } 69 | } else { 70 | /** @var Block\Template $class */ 71 | $class = $request['class']; 72 | $newParent = $class::checkAppend($parentNode, $lines, $request, $stopOnContent); 73 | if (!is_null($newParent)) { 74 | $parentNode = $newParent; 75 | } 76 | } 77 | 78 | if ($request['class'] === '\Manner\Block\Text' || $parentNode->textContent !== '') { 79 | if ($stopOnContent) { 80 | return true; 81 | } 82 | if ($request['class'] !== '\Manner\Inline\FontOneInputLine') { // TODO: hack? fix? 83 | $newParent = Man::instance()->runPostOutputCallbacks(); 84 | if (!is_null($newParent)) { 85 | $parentNode = $newParent; 86 | } 87 | } 88 | } 89 | } 90 | 91 | return !$stopOnContent; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /lib/Roff/Alias.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Manner\Man; 25 | 26 | class Alias implements Template 27 | { 28 | 29 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 30 | { 31 | array_shift($lines); 32 | Man::instance()->addAlias($request['arguments'][0], $request['arguments'][1]); 33 | } 34 | 35 | public static function check(string $requestName): string 36 | { 37 | $aliases = Man::instance()->getAliases(); 38 | if (array_key_exists($requestName, $aliases)) { 39 | $requestName = $aliases[$requestName]; 40 | } 41 | 42 | return $requestName; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /lib/Roff/Char.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Manner\Man; 25 | 26 | class Char implements Template 27 | { 28 | 29 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 30 | { 31 | array_shift($lines); 32 | if (count($request['arguments']) === 2) { 33 | Man::instance()->setEntity($request['arguments'][0], $request['arguments'][1]); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /lib/Roff/Comment.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Exception; 25 | use Manner\Replace; 26 | 27 | class Comment 28 | { 29 | 30 | /** 31 | * @param array $lines 32 | * @return bool 33 | * @throws Exception 34 | */ 35 | public static function checkLine(array &$lines): bool 36 | { 37 | // Skip full-line comments 38 | // See mscore.1 for full-line comments starting with '." 39 | // See cal3d_converter.1 for full-line comments starting with ''' 40 | // See e.g. flow-import.1 for comment starting with .\\" 41 | // See e.g. card.1 for comment starting with ." 42 | // See e.g. node.1 for comment starting with .\ 43 | // See e.g. units.1 for comment in a .de starting with . \" 44 | if (preg_match('~^([\'.]?\\\\?\\\\"|\.?\s*\\\\"|\'\."\'|\'\'\'|\."|\.\\\\\s+)~u', $lines[0], $matches)) { 45 | array_shift($lines); 46 | 47 | return true; 48 | } 49 | 50 | // \" is start of a comment. Everything up to the end of the line is ignored. 51 | // Some man pages get this wrong and expect \" to be printed (see fox-calculator.1), 52 | // but this behaviour is consistent with what the man command renders: 53 | $lines[0] = Replace::preg('~(^|.*?[^\\\\])\\\\".*$~u', '$1', $lines[0], -1, $replacements); 54 | if ($replacements > 0) { 55 | $lines[0] = mb_rtrim($lines[0], "\t"); 56 | 57 | // Look at this same line again: 58 | return true; 59 | } 60 | 61 | if (preg_match('~^[\'.]\s*ig(?:\s+(?.*)|$)~u', $lines[0], $matches)) { 62 | array_shift($lines); 63 | $delimiter = empty($matches['delimiter']) ? '..' : ('.' . $matches['delimiter']); 64 | while (count($lines)) { 65 | $line = array_shift($lines); 66 | if ($line === $delimiter) { 67 | return true; 68 | } 69 | } 70 | throw new Exception($matches[0] . ' with no corresponding "' . $delimiter . '"'); 71 | } 72 | 73 | return false; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /lib/Roff/Loop.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Exception; 25 | 26 | class Loop implements Template 27 | { 28 | 29 | /** 30 | * @param array $request 31 | * @param array $lines 32 | * @param array|null $macroArguments 33 | * @throws Exception 34 | */ 35 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 36 | { 37 | array_shift($lines); 38 | 39 | if (mb_strlen($request['raw_arg_string']) === 0) { 40 | return; // Just skip 41 | } 42 | 43 | if (preg_match( 44 | '~^' . Condition::CONDITION_REGEX . ' \\\\{\s*(.*)$~u', 45 | $request['raw_arg_string'], 46 | $matches 47 | ) 48 | ) { 49 | $unrollOne = Condition::test($matches[1], $macroArguments); 50 | $newLines = Condition::ifBlock($lines, $matches[2], $unrollOne); 51 | 52 | if ($unrollOne) { 53 | $newLines = [...$newLines, '.while ' . $matches[1] . ' \\{', ...$newLines, '\\}']; 54 | array_splice($lines, 0, 0, $newLines); 55 | } 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /lib/Roff/Macro.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Manner\Man; 25 | use Manner\Request; 26 | 27 | class Macro 28 | { 29 | 30 | public static function applyReplacements(string $string, array &$arguments, bool $fullLine = false): string 31 | { 32 | if ($fullLine) { 33 | $request = Request::peepAt($string); 34 | if ($request['name'] === 'shift') { 35 | $argsToShift = 1; 36 | if (preg_match('~^\d+$~', $request['raw_arg_string'])) { 37 | $argsToShift = (int)$request['raw_arg_string']; 38 | } 39 | for ($i = 0; $i < $argsToShift; ++$i) { 40 | array_shift($arguments); 41 | } 42 | Man::instance()->setRegister('.$', (string)count($arguments)); 43 | 44 | return '.'; 45 | } 46 | } 47 | 48 | // \$x - Macro or string argument with one-digit number x in the range 1 to 9. 49 | for ($n = 1; $n < 10; ++$n) { 50 | $string = str_replace('\\$' . $n, @$arguments[$n - 1] ?: '', $string); 51 | } 52 | 53 | // \$* : In a macro or string, the concatenation of all the arguments separated by spaces. 54 | $string = str_replace('\\$*', implode(' ', $arguments), $string); 55 | 56 | // \$@ : In a macro or string, the concatenation of all the arguments with each surrounded by double quotes, and separated by spaces. 57 | return str_replace('\\$@', '"' . implode('" "', $arguments) . '"', $string); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /lib/Roff/Register.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Exception; 25 | use Manner\Man; 26 | use Manner\Replace; 27 | 28 | class Register implements Template 29 | { 30 | 31 | /** 32 | * @param array $request 33 | * @param array $lines 34 | * @param array|null $macroArguments 35 | * @throws Exception 36 | */ 37 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 38 | { 39 | $man = Man::instance(); 40 | array_shift($lines); 41 | 42 | // Remove register 43 | if ($request['request'] === 'rr') { 44 | if (count($request['arguments']) === 1) { 45 | $man->unsetRegister($request['arguments'][0]); 46 | } 47 | 48 | return; 49 | } 50 | 51 | // .nr register ±N [M] 52 | // Define or modify register using ±N with auto-increment M 53 | 54 | if (count($request['arguments']) < 2) { 55 | return; 56 | } 57 | 58 | // Step might be in $request['arguments'][2] - but we just assume step is 1 for now. 59 | 60 | // Normalize here: a unit value may be concatenated when the register is used. 61 | $registerValue = Unit::normalize($request['arguments'][1], 'u', 'u'); 62 | $man->setRegister($request['arguments'][0], $registerValue); 63 | } 64 | 65 | public static function substitute(string $string, array &$replacements): string 66 | { 67 | return Replace::pregCallback( 68 | '~(?J)(?(?:\\\\\\\\)*)\\\\n(?\+)?(?:\[(?[^]]+)]|\((?..)|(?.))~u', 69 | function ($matches) use (&$replacements) { 70 | if (isset($replacements[$matches['reg']])) { 71 | if ($matches['op'] === '+') { 72 | $replacements[$matches['reg']] = (int)$replacements[$matches['reg']] + 1; 73 | } 74 | 75 | return $matches['bspairs'] . $replacements[$matches['reg']]; 76 | } else { 77 | // Match groff's behaviour: unset registers are 0 78 | return $matches['bspairs'] . '0'; 79 | } 80 | }, 81 | $string 82 | ); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /lib/Roff/Rename.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | class Rename implements Template 25 | { 26 | 27 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 28 | { 29 | array_shift($lines); // Just ignore for now! 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /lib/Roff/Template.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | interface Template 25 | { 26 | 27 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /lib/Roff/Translation.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Manner\Man; 25 | use Manner\TextContent; 26 | 27 | class Translation implements Template 28 | { 29 | 30 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 31 | { 32 | array_shift($lines); 33 | 34 | if (count($request['arguments']) === 1) { 35 | $man = Man::instance(); 36 | 37 | $translate = $request['arguments'][0]; 38 | $translate = TextContent::interpretString($translate, false); 39 | 40 | $chrArray = preg_split('~~u', $translate, -1, PREG_SPLIT_NO_EMPTY); 41 | 42 | for ($j = 0; $j < count($chrArray); $j += 2) { 43 | // "If there is an odd number of arguments, the last one is translated to an unstretchable space (‘\ ’)." 44 | $man->setCharTranslation($chrArray[$j], $j === count($chrArray) - 1 ? ' ' : $chrArray[$j + 1]); 45 | } 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /lib/Roff/am.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Exception; 25 | 26 | class am implements Template 27 | { 28 | 29 | /** 30 | * @param array $request 31 | * @param array $lines 32 | * @param array|null $macroArguments 33 | * @throws Exception 34 | */ 35 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 36 | { 37 | array_shift($lines); 38 | 39 | if ( 40 | !count($request['arguments']) 41 | || ($request['arguments'][0] !== 'URL' && $request['arguments'][0] !== 'MTO') 42 | ) { 43 | throw new Exception('Unexpected .am arguments: ' . print_r($request['arguments'], true)); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /lib/Roff/asRequest.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Manner\Man; 25 | 26 | // Can't just be called "as" 27 | class asRequest implements Template 28 | { 29 | 30 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 31 | { 32 | array_shift($lines); 33 | 34 | if (count($request['arguments']) === 2) { 35 | $man = Man::instance(); 36 | $stringName = $request['arguments'][0]; 37 | $appendVal = $man->applyAllReplacements($request['arguments'][1]); 38 | $appendVal = Macro::applyReplacements($appendVal, $macroArguments); 39 | $string = $man->getString($stringName); 40 | $man->addString($stringName, $string . $appendVal); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /lib/Roff/cc.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Manner\Man; 25 | 26 | class cc implements Template 27 | { 28 | 29 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 30 | { 31 | array_shift($lines); 32 | 33 | $char = count($request['arguments']) ? $request['arguments'][0] : '.'; 34 | 35 | Man::instance()->control_char = $char; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /lib/Roff/de.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Exception; 25 | use Manner\Man; 26 | use Manner\Request; 27 | 28 | class de implements Template 29 | { 30 | 31 | /** 32 | * @param array $request 33 | * @param array $lines 34 | * @param array|null $macroArguments 35 | * @throws Exception 36 | */ 37 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 38 | { 39 | // shift .de 40 | array_shift($lines); 41 | 42 | if (!preg_match('~^([^\s"]+)\s*$~u', $request['arg_string'], $matches)) { 43 | throw new Exception('Unexpected argument in \Roff\de: ' . $request['arg_string']); 44 | } 45 | 46 | $newMacro = $matches[1]; 47 | $macroLines = []; 48 | $foundEnd = false; 49 | 50 | // We don't want to handle the lines at this stage (e.g. a conditional in the macro), so don't iterate with 51 | // Request::getLine() 52 | while (count($lines)) { 53 | $line = array_shift($lines); 54 | $request = Request::peepAt($line); 55 | if ( 56 | $request['name'] === '.' || 57 | ($newMacro === 'P!' && $line === '.') // work around bug in Xm*.3 man pages 58 | ) { 59 | $foundEnd = true; 60 | break; 61 | } 62 | $macroLines[] = Request::massageLine($line); 63 | } 64 | 65 | if (!$foundEnd) { 66 | throw new Exception('Macro definition for "' . $matches[1] . '" does not follow expected pattern.'); 67 | } 68 | 69 | // Don't override these macros. 70 | // djvm e.g. does something dodgy when overriding .SS, just use normal .SS handling for it. 71 | // .URL: we can do a better job with the semantic info. 72 | // .BB & .EB: see criu.8: does something tricky with .di across macros. 73 | $protectedMacros = ['SS', 'MTO', 'URL', 'SY', 'YS', 'SH', 'TP', 'RS', 'RE', 'BB', 'EB', 'MR']; 74 | 75 | if (!in_array($newMacro, $protectedMacros)) { 76 | Man::instance()->addMacro($newMacro, $macroLines); 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /lib/Roff/di.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Exception; 25 | use Manner\Request; 26 | 27 | class di implements Template 28 | { 29 | 30 | /** 31 | * @param array $request 32 | * @param array $lines 33 | * @param array|null $macroArguments 34 | * @throws Exception 35 | */ 36 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 37 | { 38 | array_shift($lines); 39 | 40 | // We don't want to handle the lines at this stage as a fresh call to .di call a new \Request\di, so don't iterate 41 | // with Request::getLine() 42 | while ($line = array_shift($lines)) { 43 | if (Request::peepAt($line)['name'] === 'di') { 44 | return; 45 | } 46 | } 47 | throw new Exception('.di with no end .di'); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /lib/Roff/doRequest.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | /** 25 | * .do: "Interpret .name with compatibility mode disabled." (e.g. .do if ... ) 26 | * NB: we many pick up new .do calls e.g. in conditional statements. 27 | */ 28 | class doRequest implements Template 29 | { 30 | 31 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 32 | { 33 | $lines[0] = '.' . $request['raw_arg_string']; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /lib/Roff/ec.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Manner\Man; 25 | 26 | /** 27 | * Sets/resets the escape character, only ever used to reset after not being changed in body of man pages. 28 | */ 29 | class ec implements Template 30 | { 31 | 32 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 33 | { 34 | array_shift($lines); 35 | Man::instance()->escape_char = count($request['arguments']) ? $request['arguments'][0] : '\\'; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /lib/Roff/eo.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | use Manner\Man; 25 | 26 | /** 27 | * Turn off escape character mechanism. 28 | */ 29 | class eo implements Template 30 | { 31 | 32 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 33 | { 34 | array_shift($lines); 35 | Man::instance()->escape_char = null; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /lib/Roff/nop.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | class nop implements Template 25 | { 26 | 27 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 28 | { 29 | $lines[0] = $request['raw_arg_string']; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /lib/Roff/returnRequest.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner\Roff; 23 | 24 | class returnRequest implements Template 25 | { 26 | 27 | public static function evaluate(array $request, array &$lines, ?array $macroArguments): void 28 | { 29 | array_shift($lines); 30 | 31 | while (count($lines) && !is_null($lines[0])) { 32 | array_shift($lines); 33 | } 34 | 35 | // shift the null 36 | array_shift($lines); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /lib/Text.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | namespace Manner; 23 | 24 | class Text 25 | { 26 | 27 | public const string ZERO_WIDTH_SPACE_UTF8 = "\xE2\x80\x8B"; 28 | public const string ZERO_WIDTH_SPACE_HTML = '​'; 29 | 30 | public static function trimAndRemoveZWSUTF8(?string $str): string 31 | { 32 | if (is_null($str)) { 33 | return ""; 34 | } 35 | 36 | return mb_trim(str_replace(Text::ZERO_WIDTH_SPACE_UTF8, '', $str)); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /manner.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | use Manner\Manner; 23 | 24 | require_once 'autoload.php'; 25 | 26 | if (empty($argv[1])) { 27 | exit('no file.'); 28 | } 29 | 30 | $filePath = $argv[1]; 31 | 32 | if (!is_file($filePath)) { 33 | exit($filePath . ' is not a file.'); 34 | } 35 | 36 | $fileLines = file($filePath, FILE_IGNORE_NEW_LINES); 37 | 38 | try { 39 | Manner::roffToHTML($fileLines); 40 | } catch (Exception $e) { 41 | echo PHP_EOL, PHP_EOL, $e->getMessage(), PHP_EOL; 42 | echo $e->getTraceAsString(), PHP_EOL; 43 | exit(1); 44 | } 45 | -------------------------------------------------------------------------------- /run_tests.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | . 19 | */ 20 | declare(strict_types=1); 21 | 22 | use Manner\Manner; 23 | 24 | require_once 'autoload.php'; 25 | 26 | /** 27 | * @param $filePath 28 | * @throws Exception 29 | */ 30 | function runTest($filePath): void 31 | { 32 | if (!is_file($filePath)) { 33 | exit($filePath . ' is not a file.'); 34 | } 35 | 36 | $expectedOutputPath = $filePath . '.html'; 37 | 38 | if (!is_file($expectedOutputPath)) { 39 | exit($expectedOutputPath . ' is not a file.'); 40 | } 41 | 42 | $fileLines = file($filePath, FILE_IGNORE_NEW_LINES); 43 | 44 | ob_start(); 45 | Manner::roffToHTML($fileLines, null, true); 46 | $actualOutput = ob_get_contents(); 47 | ob_end_clean(); 48 | 49 | if ($actualOutput !== file_get_contents($expectedOutputPath)) { 50 | echo $filePath, PHP_EOL; 51 | echo '---------------------------', PHP_EOL; 52 | echo implode(PHP_EOL, $fileLines), PHP_EOL; 53 | echo '---------------------------', PHP_EOL; 54 | echo 'Expected:', PHP_EOL; 55 | echo file_get_contents($expectedOutputPath); 56 | echo '---------------------------', PHP_EOL; 57 | echo 'Got:', PHP_EOL; 58 | echo $actualOutput, PHP_EOL; 59 | } 60 | } 61 | 62 | $dir = new DirectoryIterator(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'tests'); 63 | foreach ($dir as $fileInfo) { 64 | if ($fileInfo->isDot()) { 65 | continue; 66 | } 67 | if ($fileInfo->getExtension() === 'html') { 68 | continue; 69 | } 70 | runTest($fileInfo->getRealPath()); 71 | } 72 | -------------------------------------------------------------------------------- /tests/0in.1: -------------------------------------------------------------------------------- 1 | .TH 0IN 1 2 | .SH OPTIONS 3 | 4 | The first non-option argument to 0install is the particular sub-command you 5 | want to perform; these are described in detail in the next section. 6 | 7 | However, 8 | -------------------------------------------------------------------------------- /tests/0in.1.html: -------------------------------------------------------------------------------- 1 |

0IN

OPTIONS

The first non-option argument to 0install is the particular sub-command you want to perform; these are described in detail in the next section.

However,

2 | -------------------------------------------------------------------------------- /tests/BR.1: -------------------------------------------------------------------------------- 1 | .TH BR 2 | .SH HEADING 3 | See 4 | .BR man (1) 5 | -------------------------------------------------------------------------------- /tests/BR.1.html: -------------------------------------------------------------------------------- 1 |

BR

HEADING

See man(1)

2 | -------------------------------------------------------------------------------- /tests/FONT.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .de FONT 4 | . ds result \& 5 | . while (\\n[.$] >= 2) \{\ 6 | . as result \,\f[\\$1]\\$2 7 | . if !"\\$1"P" .as result \f[P]\"" 8 | . shift 2 9 | . \} 10 | . if (\\n[.$] = 1) .as result \,\f[\\$1] 11 | . nh 12 | . nop \\*[result]\& 13 | . hy 14 | .. 15 | .FONT B \[rs]*[ I B ] 16 | -------------------------------------------------------------------------------- /tests/FONT.1.html: -------------------------------------------------------------------------------- 1 |

A

H

\*[<colorname>]

2 | -------------------------------------------------------------------------------- /tests/FontOneInputLine.1: -------------------------------------------------------------------------------- 1 | .TH test 2 | .SH HEADING 3 | .B bold 4 | .I 5 | em 6 | -------------------------------------------------------------------------------- /tests/FontOneInputLine.1.html: -------------------------------------------------------------------------------- 1 |

test

HEADING

bold em

2 | -------------------------------------------------------------------------------- /tests/IPs.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .IP * 4 4 | A 5 | . 6 | .IP "" 0 7 | . 8 | .P 9 | New para 10 | -------------------------------------------------------------------------------- /tests/IPs.1.html: -------------------------------------------------------------------------------- 1 |

A

H

  • A

New para

2 | -------------------------------------------------------------------------------- /tests/MT.1: -------------------------------------------------------------------------------- 1 | .TH MT 2 | .PP 3 | Mail comments, suggestions and bug reports to 4 | .MT shaleh@\:\:valinux.\:com 5 | Sean 'Shaleh' Perry 6 | .ME . 7 | -------------------------------------------------------------------------------- /tests/MT.1.html: -------------------------------------------------------------------------------- 1 |

MT

Mail comments, suggestions and bug reports to Sean 'Shaleh' Perry.

2 | -------------------------------------------------------------------------------- /tests/NOP.1: -------------------------------------------------------------------------------- 1 | .TH NOP 2 | .de1 NOP 3 | . it 1 an-trap 4 | . if \\n[.$] \,\\$*\/ 5 | .. 6 | .NOP hey 7 | -------------------------------------------------------------------------------- /tests/NOP.1.html: -------------------------------------------------------------------------------- 1 |

NOP

hey

2 | -------------------------------------------------------------------------------- /tests/SbC2.1: -------------------------------------------------------------------------------- 1 | '\"! tbl | mmdoc 2 | '\"macro stdmacro 3 | . ds Cr \fB 4 | . ds Cb \fB 5 | .TH SbCylinder(3IV) 6 | .SH NAME 7 | .ds Pt \*(Cr 8 | .ie \w'\*(Pt'>=20n \{\ 9 | .ne 3 10 | \*(Pt 11 | .ti 0.5i 12 | \c\ 13 | \} 14 | .el\{\ 15 | .ne 2 16 | \*(Pt \c\ 17 | \} 18 | \*(CbSbCyl 19 | -------------------------------------------------------------------------------- /tests/SbC2.1.html: -------------------------------------------------------------------------------- 1 |

SbCylinder(3IV)

NAME

  SbCyl

2 | -------------------------------------------------------------------------------- /tests/SbCylinder.3iv: -------------------------------------------------------------------------------- 1 | '\"! tbl | mmdoc 2 | '\"macro stdmacro 3 | .ie n \{\ 4 | . ds Cr \fB 5 | . ds Cb \fB 6 | .\} 7 | .el \{\ 8 | . ds Cr \f7 9 | . ds Cb \f8 10 | .\} 11 | .TH SbCylinder(3IV) 12 | .SH SYNOPSIS 13 | .ps -1 14 | \*(Cr#include 15 | -------------------------------------------------------------------------------- /tests/SbCylinder.3iv.html: -------------------------------------------------------------------------------- 1 |

SbCylinder(3IV)

SYNOPSIS

#include <Inventor/SbLinear.h>

2 | -------------------------------------------------------------------------------- /tests/SoXtWalkViewer-b.3: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .if \w'a b'>=2n \{\ 4 | .ti 1 5 | \c\ 6 | \} 7 | A 8 | -------------------------------------------------------------------------------- /tests/SoXtWalkViewer-b.3.html: -------------------------------------------------------------------------------- 1 |

A

H

 A

2 | -------------------------------------------------------------------------------- /tests/SoXtWalkViewer.3: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .ds Pt \*(Crvirtual void 4 | .if \w'\*(Pt'>=24n \{\ 5 | .ti 1 6 | \c\ 7 | \} 8 | A 9 | -------------------------------------------------------------------------------- /tests/SoXtWalkViewer.3.html: -------------------------------------------------------------------------------- 1 |

A

H

A

2 | -------------------------------------------------------------------------------- /tests/ZN.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .de ZN 4 | .ie t \fB\^\\$1\^\fR\\$2 5 | .el \fI\^\\$1\^\fP\\$2 6 | .. 7 | .IP A 8 | .ZN B 9 | .LP 10 | .ZN C 11 | D 12 | .ZN 13 | -------------------------------------------------------------------------------- /tests/ZN.1.html: -------------------------------------------------------------------------------- 1 |

A

H

A

B

C D

2 | -------------------------------------------------------------------------------- /tests/apropos.1: -------------------------------------------------------------------------------- 1 | .TH blah 1 2 | .SH OPTIONS 3 | .TP 4 | A 5 | B 6 | .\" 7 | .\" Due to the rather silly limit of 6 args per request in some `native' 8 | .\" *roff compilers, we have do the following to get the two-line 9 | .\" hanging tag on one line. .PP to begin a new paragraph, then the 10 | .\" tag, then .RS (start relative indent), the text, finally .RE 11 | .\" (end relative indent). 12 | .\" 13 | .PP 14 | A 15 | .RS 16 | Sentence 1 17 | 18 | Sentence 2 19 | .RE 20 | .TP 21 | A 22 | B 23 | -------------------------------------------------------------------------------- /tests/apropos.1.html: -------------------------------------------------------------------------------- 1 |

blah

OPTIONS

A

B

A

Sentence 1

Sentence 2

A

B

2 | -------------------------------------------------------------------------------- /tests/atop.1: -------------------------------------------------------------------------------- 1 | .TH atop 2 | .SH HEADING 3 | .PP 4 | .TP 4 5 | term 6 | def 7 | .PP 8 | -------------------------------------------------------------------------------- /tests/atop.1.html: -------------------------------------------------------------------------------- 1 |

atop

HEADING

term

def

2 | -------------------------------------------------------------------------------- /tests/b.1: -------------------------------------------------------------------------------- 1 | .TH blah 2 | .SH NAME 3 | .B 4 | A 5 | .B " 6 | B 7 | -------------------------------------------------------------------------------- /tests/b.1.html: -------------------------------------------------------------------------------- 1 |

blah

NAME

A B

2 | -------------------------------------------------------------------------------- /tests/bash_fonts.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | to "\fB$1\fP\fIc\fP\fB$2\fP\fIc\fP\fB...\fP", where 4 | .I c 5 | is 6 | -------------------------------------------------------------------------------- /tests/bash_fonts.1.html: -------------------------------------------------------------------------------- 1 |

A

H

to "$1c$2c...", where c is

2 | -------------------------------------------------------------------------------- /tests/bash_sm.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .SM 4 | \fBCOMMAND EXECUTION ENVIRONMENT\fP 5 | below). 6 | Variable 7 | -------------------------------------------------------------------------------- /tests/bash_sm.1.html: -------------------------------------------------------------------------------- 1 |

A

H

COMMAND EXECUTION ENVIRONMENT below). Variable

2 | -------------------------------------------------------------------------------- /tests/br_in_tp.1: -------------------------------------------------------------------------------- 1 | .TH BRinTP 1 2 | .SH Heading 3 | .TP 4 | term 5 | A 6 | .br 7 | B 8 | -------------------------------------------------------------------------------- /tests/br_in_tp.1.html: -------------------------------------------------------------------------------- 1 |

BRinTP

Heading

term

A
B

2 | -------------------------------------------------------------------------------- /tests/bull.1: -------------------------------------------------------------------------------- 1 | .TH BULL 1 2 | .SH BLAH 3 | a\c 4 | 5 | heh 6 | -------------------------------------------------------------------------------- /tests/bull.1.html: -------------------------------------------------------------------------------- 1 |

BULL

BLAH

a heh

2 | -------------------------------------------------------------------------------- /tests/c.3: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | . ds Cr \fB 4 | .ds Pt \*(Crvirtual void 5 | .ie \w'\*(Pt'>=24n \{\ 6 | .ne 3 7 | \*(Pt 8 | .ti 0.5i 9 | \c\ 10 | \} 11 | .el\{\ 12 | .ne 2 13 | \*(Pt \c\ 14 | \} 15 | B 16 | -------------------------------------------------------------------------------- /tests/c.3.html: -------------------------------------------------------------------------------- 1 |

A

H

virtual void   B

2 | -------------------------------------------------------------------------------- /tests/callerid.conf.5: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .TP 3 | \\$1 4 | flags... 5 | -------------------------------------------------------------------------------- /tests/callerid.conf.5.html: -------------------------------------------------------------------------------- 1 |

A

\$1

flags...

2 | -------------------------------------------------------------------------------- /tests/cc.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .nf 4 | .cc | 5 | mode = LIST 6 | separator = "|" 7 | main prompt = "sqlite> " 8 | continue prompt = " ...> " 9 | |cc . 10 | .sp 11 | .fi 12 | -------------------------------------------------------------------------------- /tests/cc.1.html: -------------------------------------------------------------------------------- 1 |

A

H

mode            = LIST
2 | separator       = "|"
3 | main prompt     = "sqlite> "
4 | continue prompt = "   ...> "
5 | -------------------------------------------------------------------------------- /tests/cef5conv.1: -------------------------------------------------------------------------------- 1 | .TH cef5conv 2 | .if n \{\ 3 | .de C 4 | \\$1 5 | .. 6 | .\} 7 | .SH AUTHOR 8 | Werner Lemberg 9 | .C 10 | -------------------------------------------------------------------------------- /tests/cef5conv.1.html: -------------------------------------------------------------------------------- 1 |

cef5conv

AUTHOR

Werner Lemberg

2 | -------------------------------------------------------------------------------- /tests/chars.1: -------------------------------------------------------------------------------- 1 | .TH chars 2 | .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' 3 | .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' 4 | .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] 5 | .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] 6 | 7 | . ds d- d\h'-1'\(ga 8 | . ds D- D\h'-1'\(hy 9 | . ds th \o'bp' 10 | . ds Th \o'LP' 11 | 12 | \*(d- 13 | 14 | \*(D- 15 | 16 | \*(th 17 | 18 | \*(Th 19 | -------------------------------------------------------------------------------- /tests/chars.1.html: -------------------------------------------------------------------------------- 1 |

chars

ð

Ð

Þ

þ

2 | -------------------------------------------------------------------------------- /tests/ci.2: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .nr n \w'\(bu'+2n-1/1n 4 | .ds n \nn 5 | \*n 6 | ." .if \n(.g .if r an-tag-sep .ds n \w'\(bu'u+\n[an-tag-sep]u 7 | ." \*n 8 | -------------------------------------------------------------------------------- /tests/ci.2.html: -------------------------------------------------------------------------------- 1 |

A

H

2.7142857142857

2 | -------------------------------------------------------------------------------- /tests/comment_after_font.1: -------------------------------------------------------------------------------- 1 | .TH comment 2 | .de NS 3 | .sp 4 | .in +4 5 | .nf 6 | .ft C \" Courier 7 | .. 8 | .de NE 9 | .fi 10 | .ft R 11 | .in -4 12 | .. 13 | .NS 14 | hey 15 | .NE 16 | -------------------------------------------------------------------------------- /tests/comment_after_font.1.html: -------------------------------------------------------------------------------- 1 |

comment

hey
2 | -------------------------------------------------------------------------------- /tests/comp.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | Text 4 | .EX 5 | .ta \w'Escape 'u +\w'Returns 'u 6 | .I "Escape Returns Description" 7 | fcc string Any folders specified with `\-fcc\ folder' 8 | subject string Any text specified with `\-subject\ text' 9 | .EE 10 | Text 11 | -------------------------------------------------------------------------------- /tests/comp.1.html: -------------------------------------------------------------------------------- 1 |

A

H

Text

EscapeReturnsDescription
fccstringAny folders specified with `-fcc folder'
subjectstringAny text specified with `-subject text'

Text

2 | -------------------------------------------------------------------------------- /tests/cpupower-monitor.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH Options 3 | .PP 4 | \-l 5 | .RS 4 6 | List available monitors... 7 | .RS 2 8 | .IP \(bu 9 | The amount of... 10 | .IP \(bu 11 | The name and... 12 | .RS 4 13 | .IP \(bu 14 | [T] \-> Thread 15 | .IP \(bu 16 | [C] \-> Core 17 | .RE 18 | .RE 19 | .RE 20 | -------------------------------------------------------------------------------- /tests/cpupower-monitor.1.html: -------------------------------------------------------------------------------- 1 |

A

Options

-l

List available monitors...

  • The amount of...
  • The name and...

    • [T] -> Thread
    • [C] -> Core
2 | -------------------------------------------------------------------------------- /tests/customVb.1: -------------------------------------------------------------------------------- 1 | .TH customVb 2 | .de Vb \" Begin verbatim text 3 | .ft CW 4 | .nf 5 | .ne \\$1 6 | .. 7 | .de Ve \" End verbatim text 8 | .ft R 9 | .fi 10 | .. 11 | .IP A 4 12 | B 13 | .IP C 4 14 | D 15 | .Sp 16 | .Vb 3 17 | E: verbose bit 18 | .Ve 19 | .Sp 20 | F 21 | .IP G 4 22 | H 23 | -------------------------------------------------------------------------------- /tests/customVb.1.html: -------------------------------------------------------------------------------- 1 |

customVb

A

B

C

D

E: verbose bit

F

G

H

2 | -------------------------------------------------------------------------------- /tests/digit_space.1: -------------------------------------------------------------------------------- 1 | .TH hey 1 2 | .SH H 3 | .PP 4 | a\0b 5 | .PP 6 | .BR c \0 d 7 | .TP 8 | .BI e \0 f \0 g 9 | .TP 10 | -------------------------------------------------------------------------------- /tests/digit_space.1.html: -------------------------------------------------------------------------------- 1 |

hey

H

a b

c d

e f g

2 | -------------------------------------------------------------------------------- /tests/dir.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .TP 4 | \fB\-\-group\-directories\-first\fR 5 | group directories before files; 6 | .IP 7 | can be augmented with 8 | -------------------------------------------------------------------------------- /tests/dir.1.html: -------------------------------------------------------------------------------- 1 |

A

H

--group-directories-first

group directories before files;

can be augmented with

2 | -------------------------------------------------------------------------------- /tests/eh.1: -------------------------------------------------------------------------------- 1 | .TH EH 1 2 | .SH BLAH 3 | OPTIONS\c 4 | a). 5 | 6 | By 7 | -------------------------------------------------------------------------------- /tests/eh.1.html: -------------------------------------------------------------------------------- 1 |

EH

BLAH

OPTIONSa).

By

2 | -------------------------------------------------------------------------------- /tests/ep.1: -------------------------------------------------------------------------------- 1 | .TH ep 2 | .ds EP \fIC\fP 3 | A 4 | B \*(EP. 5 | -------------------------------------------------------------------------------- /tests/ep.1.html: -------------------------------------------------------------------------------- 1 |

ep

A B C.

2 | -------------------------------------------------------------------------------- /tests/eqn.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .RS 4 | .TP 5 | .B fat_offset 6 | The... 7 | .RS 8 | .IP 9 | .EX 10 | 11 | .EE 12 | .RE 13 | .TP 14 | .B over_hang 15 | A fraction bar... 16 | .RE 17 | -------------------------------------------------------------------------------- /tests/eqn.1.html: -------------------------------------------------------------------------------- 1 |

A

H

fat_offset

The...

<mstyle mathvariant='double-struck'>
over_hang

A fraction bar...

2 | -------------------------------------------------------------------------------- /tests/ex.1: -------------------------------------------------------------------------------- 1 | .TH E 1 2 | .SH BUGS 3 | Version 2.23 changed the 4 | .B \-s 5 | option to be non-greedy, for example: 6 | .PP 7 | .EX 8 | \fBprintf "a:b:c\\n1::3\\n" | column -t -s ':'\fR 9 | .EE 10 | .PP 11 | Old output: 12 | .EX 13 | a b c 14 | 1 3 15 | .EE 16 | .PP 17 | New output (since util-linux 2.23): 18 | .EX 19 | a b c 20 | 1 3 21 | .EE 22 | -------------------------------------------------------------------------------- /tests/ex.1.html: -------------------------------------------------------------------------------- 1 |

E

BUGS

Version 2.23 changed the -s option to be non-greedy, for example:

printf "a:b:c\n1::3\n" | column  -t -s ':'

Old output:

a  b  c
2 | 1  3

New output (since util-linux 2.23):

a  b  c
3 | 1     3
4 | -------------------------------------------------------------------------------- /tests/fortune.6: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .TP 4 | term 5 | start 6 | .sp 7 | .RS 8 | new para 9 | .RE 10 | -------------------------------------------------------------------------------- /tests/fortune.6.html: -------------------------------------------------------------------------------- 1 |

A

H

term

start

new para

2 | -------------------------------------------------------------------------------- /tests/gawk.1b: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .BI \e n\fR, 4 | where 5 | -------------------------------------------------------------------------------- /tests/gawk.1b.html: -------------------------------------------------------------------------------- 1 |

A

H

\n, where

2 | -------------------------------------------------------------------------------- /tests/gcc.1: -------------------------------------------------------------------------------- 1 | .TH gcc 2 | .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' 3 | A B \*(C+ D 4 | -------------------------------------------------------------------------------- /tests/gcc.1.html: -------------------------------------------------------------------------------- 1 |

gcc

A B C++ D

2 | -------------------------------------------------------------------------------- /tests/gdiffmk.1: -------------------------------------------------------------------------------- 1 | .TH A 2 | .SH H 3 | This document was written and is maintained by 4 | .MT MBianchi@Foveal.com 5 | Mike Bianchi 6 | .MT . 7 | -------------------------------------------------------------------------------- /tests/gdiffmk.1.html: -------------------------------------------------------------------------------- 1 |

A

H

This document was written and is maintained by Mike Bianchi

2 | -------------------------------------------------------------------------------- /tests/gifsicle.1: -------------------------------------------------------------------------------- 1 | .TH GIFSICLE 1 "5 May 2012" "Version \*V" 2 | .SH SYNOPSIS 3 | A 4 | ' 5 | . 6 | -------------------------------------------------------------------------------- /tests/gifsicle.1.html: -------------------------------------------------------------------------------- 1 |

GIFSICLE

SYNOPSIS

A

2 | -------------------------------------------------------------------------------- /tests/glue-validator.1: -------------------------------------------------------------------------------- 1 | .TH GLUE_VALIDATOR 1 2 | .SH OPTIONS 3 | .IP "-t --test" 4 | The test class [glue1|glue2]. 5 | .PP 6 | Server Mode: ... 7 | .IP "-h --host" 8 | Hostname of the LDAP server. 9 | -------------------------------------------------------------------------------- /tests/glue-validator.1.html: -------------------------------------------------------------------------------- 1 |

GLUE_VALIDATOR

OPTIONS

-t --test

The test class [glue1|glue2].

Server Mode: ...

-h --host

Hostname of the LDAP server.

2 | -------------------------------------------------------------------------------- /tests/gmusicbrowser.1: -------------------------------------------------------------------------------- 1 | .TH gmusicbrowser 2 | .B 3 | .TP 4 | A 5 | B 6 | -------------------------------------------------------------------------------- /tests/gmusicbrowser.1.html: -------------------------------------------------------------------------------- 1 |

gmusicbrowser

A

B

2 | -------------------------------------------------------------------------------- /tests/gpp.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .P 4 | Moreover, all of these matching subsets except `\\w' and `\\W' can be 5 | -------------------------------------------------------------------------------- /tests/gpp.1.html: -------------------------------------------------------------------------------- 1 |

A

H

Moreover, all of these matching subsets except `\w' and `\W' can be

2 | -------------------------------------------------------------------------------- /tests/groff_mm.7: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .BI AL\ \fR[\fPtype\ \fR[\fPtext-indent\ \fR[\fP1\fR]]]\fP 4 | -------------------------------------------------------------------------------- /tests/groff_mm.7.html: -------------------------------------------------------------------------------- 1 |

A

H

AL [type [text-indent [1]]]

2 | -------------------------------------------------------------------------------- /tests/hg.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | A 4 | .sp 5 | .nf 6 | .ft C 7 | B 8 | .ft P 9 | .fi 10 | C 11 | -------------------------------------------------------------------------------- /tests/hg.1.html: -------------------------------------------------------------------------------- 1 |

A

H

A

B

C

2 | -------------------------------------------------------------------------------- /tests/indentation-a.1: -------------------------------------------------------------------------------- 1 | .TH Indentation-A 1 2 | .SH Heading 3 | .TP 4 | term 5 | stuff 6 | .RS 7 | .TP 20 8 | wide 9 | apart 10 | .RE 11 | .IP 12 | indented para 13 | -------------------------------------------------------------------------------- /tests/indentation-a.1.html: -------------------------------------------------------------------------------- 1 |

Indentation-A

Heading

term

stuff

wide

apart

indented para

2 | -------------------------------------------------------------------------------- /tests/innxbatch.8: -------------------------------------------------------------------------------- 1 | .TH innxbatch 2 | .ds R$ A 3 | \*(R$ 4 | .ds R$ B 5 | \*(R$ 6 | .nf 7 | .ds R$ C 8 | \*(R$ 9 | .ds R$ D 10 | \*(R$ 11 | .fi 12 | -------------------------------------------------------------------------------- /tests/innxbatch.8.html: -------------------------------------------------------------------------------- 1 |

innxbatch

A B

C
2 | D
3 | -------------------------------------------------------------------------------- /tests/iostat.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .IP Report 4 | The report has the following format: 5 | 6 | .B %user 7 | .RS 8 | .RS 9 | Show the percentage of CPU utilization that occurred while 10 | .RE 11 | 12 | .B %nice 13 | .RS 14 | Show the percentage of CPU utilization that occurred while 15 | .RE 16 | 17 | .B %system 18 | .RS 19 | Show the percentage of CPU utilization that occurred while 20 | .RE 21 | .RE 22 | -------------------------------------------------------------------------------- /tests/iostat.1.html: -------------------------------------------------------------------------------- 1 |

A

H

Report

The report has the following format:

%user

Show the percentage of CPU utilization that occurred while

%nice

Show the percentage of CPU utilization that occurred while

%system

Show the percentage of CPU utilization that occurred while

2 | -------------------------------------------------------------------------------- /tests/ksh.1: -------------------------------------------------------------------------------- 1 | .nr Z 1 \" set to 1 when command name is ksh, 2 for ksh93 2 | .if \nZ=0 \{\ 3 | .TH SH 1 4 | .\} 5 | .if \nZ=1 \{\ 6 | .TH KSH 1 7 | .\} 8 | .SH H 9 | -------------------------------------------------------------------------------- /tests/ksh.1.html: -------------------------------------------------------------------------------- 1 |

KSH

H

2 | -------------------------------------------------------------------------------- /tests/links2.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH FILES 3 | .TP 4 | .IP "~/.links/links.cfg" 5 | Per-user configfile, automatically created by 6 | .B links. 7 | .TP 8 | ~/.links/links.cfg 9 | Per-user configfile, automatically created by 10 | .B links. 11 | -------------------------------------------------------------------------------- /tests/links2.1.html: -------------------------------------------------------------------------------- 1 |

A

FILES

~/.links/links.cfg

Per-user configfile, automatically created by links.

~/.links/links.cfg

Per-user configfile, automatically created by links.

2 | -------------------------------------------------------------------------------- /tests/listA.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | Something: 4 | .RS 4 5 | \(bu one 6 | .RE 7 | .RS 4 8 | \(bu two 9 | .RE 10 | .RS 4 11 | \(bu three 12 | .RE 13 | -------------------------------------------------------------------------------- /tests/listA.1.html: -------------------------------------------------------------------------------- 1 |

A

H

Something:

  • one
  • two
  • three
2 | -------------------------------------------------------------------------------- /tests/logical_or.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .if (1:(0)) yep 4 | -------------------------------------------------------------------------------- /tests/logical_or.1.html: -------------------------------------------------------------------------------- 1 |

A

H

yep

2 | -------------------------------------------------------------------------------- /tests/lsmcli.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | You can pass... 4 | .RS 4 5 | * Using a 6 | .br 7 | * Using b 8 | .br 9 | * Add c 10 | .RS 4 11 | blah 12 | -------------------------------------------------------------------------------- /tests/lsmcli.1.html: -------------------------------------------------------------------------------- 1 |

A

H

You can pass...

  • Using a
  • Using b
  • Add c

    blah

2 | -------------------------------------------------------------------------------- /tests/mdadm.8: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .ie '1.2'0.90' 3 | Bad 4 | .el 5 | Good 6 | -------------------------------------------------------------------------------- /tests/mdadm.8.html: -------------------------------------------------------------------------------- 1 |

A

Good

2 | -------------------------------------------------------------------------------- /tests/mplayer.1: -------------------------------------------------------------------------------- 1 | .TH mplayer 2 | .de IPs 3 | .IP "\\$1" 5 4 | .. 5 | .IPs 6 | mplayer \-http\-header\-fields 'Field1: value1','Field2: value2' http://localhost:1234 7 | -------------------------------------------------------------------------------- /tests/mplayer.1.html: -------------------------------------------------------------------------------- 1 |

mplayer

mplayer -http-header-fields 'Field1: value1','Field2: value2' http://localhost:1234

2 | -------------------------------------------------------------------------------- /tests/nawk.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .TP 4 | .EX 5 | /start/, /stop/ 6 | .EE 7 | Print all lines between start/stop pairs. 8 | -------------------------------------------------------------------------------- /tests/nawk.1.html: -------------------------------------------------------------------------------- 1 |

A

H

/start/, /stop/

Print all lines between start/stop pairs.

2 | -------------------------------------------------------------------------------- /tests/nf.1: -------------------------------------------------------------------------------- 1 | .TH nf 2 | .nf 3 | A 4 | .ds R$ B 5 | \*(R$ 6 | .ds R$ C 7 | \*(R$ 8 | -------------------------------------------------------------------------------- /tests/nf.1.html: -------------------------------------------------------------------------------- 1 |

nf

A
2 |     B
3 |     C
4 | -------------------------------------------------------------------------------- /tests/ovn-sb.5: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .IP 4 | The following actions are defined: 5 | .RS 6 | .TP 7 | \fBct_snat;\fR 8 | .TQ .5in 9 | \fBct_snat(\fIIP\fB);\fR 10 | \fBct_snat\fR sends the packet... 11 | .RS 12 | .IP \(bu 13 | On a gateway router... 14 | .IP \(bu 15 | On a distributed... 16 | .RE 17 | .IP 18 | \fBct_snat(\fIIP\fB)\fR sends the packet 19 | -------------------------------------------------------------------------------- /tests/ovn-sb.5.html: -------------------------------------------------------------------------------- 1 |

A

H

The following actions are defined:

ct_snat;
ct_snat(IP);

ct_snat sends the packet...

  • On a gateway router...
  • On a distributed...

ct_snat(IP) sends the packet

2 | -------------------------------------------------------------------------------- /tests/pmcpp.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | \fB#shell '\fIcommand\fB' 4 | .br 5 | The shell 6 | -------------------------------------------------------------------------------- /tests/pmcpp.1.html: -------------------------------------------------------------------------------- 1 |

A

H

#shell 'command'
The shell

2 | -------------------------------------------------------------------------------- /tests/pmdapipe.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .PP 4 | Each command configuration line is of the form: 5 | .TP 6 | \& 7 | \f2instance\f1 \f2username\f1 \f2command\f1 \f2options\f1 8 | .PP 9 | Where 10 | -------------------------------------------------------------------------------- /tests/pmdapipe.1.html: -------------------------------------------------------------------------------- 1 |

A

H

Each command configuration line is of the form:

instance username command options

Where

2 | -------------------------------------------------------------------------------- /tests/portage.5: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .nr step 1 1 3 | .IP \n[step]. 3 4 | A 5 | .IP \n+[step]. 6 | B 7 | .IP \n+[step]. 8 | C 9 | -------------------------------------------------------------------------------- /tests/portage.5.html: -------------------------------------------------------------------------------- 1 |

A

  1. A
  2. B
  3. C
2 | -------------------------------------------------------------------------------- /tests/pp.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | Text 4 | .PP 5 | .SH A 6 | -------------------------------------------------------------------------------- /tests/pp.1.html: -------------------------------------------------------------------------------- 1 |

A

H

Text

A

2 | -------------------------------------------------------------------------------- /tests/pp_in_pre.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .nf 4 | A 5 | .PP 6 | tool. 7 | .nf 8 | $ setreg 1 true 9 | .fi 10 | .PP 11 | Another option to ease updates is to synchronize your machine trust store 12 | 13 | -------------------------------------------------------------------------------- /tests/pp_in_pre.1.html: -------------------------------------------------------------------------------- 1 |

A

H

A
2 | 
3 | tool.
4 |         $ setreg 1 true

Another option to ease updates is to synchronize your machine trust store

5 | -------------------------------------------------------------------------------- /tests/prefontspace.2: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .ft B 4 | .nf 5 | hey 6 | .fi 7 | -------------------------------------------------------------------------------- /tests/prefontspace.2.html: -------------------------------------------------------------------------------- 1 |

A

H

  hey
2 | -------------------------------------------------------------------------------- /tests/proc.5: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .IP 4 | For... 5 | .nf 6 | pos: 0 7 | .fi 8 | event 9 | -------------------------------------------------------------------------------- /tests/proc.5.html: -------------------------------------------------------------------------------- 1 |

A

H

For...

pos:    0

event

2 | -------------------------------------------------------------------------------- /tests/ps.1: -------------------------------------------------------------------------------- 1 | .TH PS 1 "July 28, 2004" "Linux" "Linux User's Manual" 2 | .nr OptSize (16u) 3 | .de opt 4 | . TP \\n[OptSize] 5 | . BI \\$* 6 | .. 7 | .opt A 8 | B 9 | -------------------------------------------------------------------------------- /tests/ps.1.html: -------------------------------------------------------------------------------- 1 |

PS

A

B

2 | -------------------------------------------------------------------------------- /tests/rc.4: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .de Cr 3 | .Xf R R \& "\\$1" "\\$2" "\\$3" "\\$4" "\\$5" "\\$6" 4 | .. 5 | .de Xf 6 | .if !"\\$4"" .Xf \\$2 \\$1 "\\$3\\f\\$1\\$4" "\\$5" "\\$6" "\\$7" "\\$8" "\\$9" 7 | .if "\\$4"" \\$3\fR 8 | .. 9 | A 10 | .Cr $^foo , 11 | B 12 | .Cr "$""foo" 13 | C 14 | -------------------------------------------------------------------------------- /tests/rc.4.html: -------------------------------------------------------------------------------- 1 |

A

A $^foo, B $"foo C

2 | -------------------------------------------------------------------------------- /tests/rc.4b: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .de Cr 3 | .Xf R R \& "\\$1" "\\$2" "\\$3" 4 | .. 5 | .de Xf 6 | .if !"\\$4"" .Xf \\$2 \\$1 "\\$3\\f\\$1\\$4" "\\$5" "\\$6 7 | .if "\\$4"" \\$3\fR 8 | .. 9 | A 10 | .Cr $^foo , 11 | -------------------------------------------------------------------------------- /tests/rc.4b.html: -------------------------------------------------------------------------------- 1 |

A

A $^foo,

2 | -------------------------------------------------------------------------------- /tests/rc.4c: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .de Cr 3 | .Xf R R \& "\\$1" 4 | .. 5 | .de Xf 6 | .if !"\\$4"" .Xf \\$2 \\$1 "\\$3\\f\\$1\\$4" 7 | .if "\\$4"" \\$3\fR 8 | .. 9 | A 10 | .Cr B 11 | -------------------------------------------------------------------------------- /tests/rc.4c.html: -------------------------------------------------------------------------------- 1 |

A

A B

2 | -------------------------------------------------------------------------------- /tests/rc.4d: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .de Xf 3 | .if "\\$1"Q" B 4 | .. 5 | A 6 | .Xf Q 7 | -------------------------------------------------------------------------------- /tests/rc.4d.html: -------------------------------------------------------------------------------- 1 |

A

A B

2 | -------------------------------------------------------------------------------- /tests/rc.4e: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | A 3 | .if "Q"Q" B 4 | -------------------------------------------------------------------------------- /tests/rc.4e.html: -------------------------------------------------------------------------------- 1 |

A

A B

2 | -------------------------------------------------------------------------------- /tests/rc.4f: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH A" 3 | .SH "A"B" 4 | -------------------------------------------------------------------------------- /tests/rc.4f.html: -------------------------------------------------------------------------------- 1 |

A

A"

A B"

2 | -------------------------------------------------------------------------------- /tests/re_syntax.n: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .ie '\w'o''\w'\C'^o''' .ds qo \C'^o' 4 | .el .ds qo u 5 | A: \*(qo 6 | -------------------------------------------------------------------------------- /tests/re_syntax.n.html: -------------------------------------------------------------------------------- 1 |

A

H

A: ô

2 | -------------------------------------------------------------------------------- /tests/repl.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .PP 4 | .fc ^ ~ 5 | .nf 6 | .ta \w'/etc/nmh/ExtraBigFileName 'u 7 | ^/etc/nmh/replcomps~^The standard reply template 8 | ^or /replcomps~^Rather than the standard template 9 | .fi 10 | -------------------------------------------------------------------------------- /tests/repl.1.html: -------------------------------------------------------------------------------- 1 |

A

H

/etc/nmh/replcompsThe standard reply template
or <mh-dir>/replcompsRather than the standard template
2 | -------------------------------------------------------------------------------- /tests/roff.7: -------------------------------------------------------------------------------- 1 | .TH ROFF 7 "4 November 2014" "Groff Version 1.22.3" 2 | .de Esc 3 | . ds @1 \\$1 4 | . shift 5 | . nop \f[B]\[rs]\\*[@1]\f[]\\$* 6 | . rm @1 7 | .. 8 | . 9 | .de QuotedChar 10 | . ds @1 \\$1 11 | . shift 12 | . nop \[oq]\f[B]\\*[@1]\f[]\[cq]\\$* 13 | . rm @1 14 | .. 15 | .P 16 | .I Escape sequences 17 | are 18 | .I roff 19 | elements starting with a backslash 20 | .QuotedChar \[rs] . 21 | . 22 | They can be inserted anywhere, also in the midst of text in a line. 23 | . 24 | They are used to implement various features, including the insertion of 25 | non-\f[CR]ASCII\f[] characters with 26 | .Esc ( , 27 | font changes with 28 | .Esc f , 29 | in-line comments with 30 | .Esc \[dq] , 31 | the escaping of special control characters like 32 | .Esc \[rs] , 33 | and many other features. 34 | -------------------------------------------------------------------------------- /tests/roff.7.html: -------------------------------------------------------------------------------- 1 |

ROFF

Escape sequences are roff elements starting with a backslash ‘\’. They can be inserted anywhere, also in the midst of text in a line. They are used to implement various features, including the insertion of non-ASCII characters with \(, font changes with \f, in-line comments with \", the escaping of special control characters like \\, and many other features.

2 | -------------------------------------------------------------------------------- /tests/rs.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .TP 4 | A 5 | Some text. 6 | .RS 7 | C 8 | .TP 9 | D 10 | E 11 | .RE 12 | F 13 | .TP 14 | G 15 | H 16 | -------------------------------------------------------------------------------- /tests/rs.1.html: -------------------------------------------------------------------------------- 1 |

A

H

A

Some text.

C

D

E

F

G

H

2 | -------------------------------------------------------------------------------- /tests/sh.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH A B 3 | .SH "A B" 4 | .SH 5 | A B 6 | .SH \ 7 | -------------------------------------------------------------------------------- /tests/sh.1.html: -------------------------------------------------------------------------------- 1 |

A

A B

A B

A B

2 | -------------------------------------------------------------------------------- /tests/simple.1: -------------------------------------------------------------------------------- 1 | .TH simple 2 | .SH HEADING 3 | .SS SUBHEADING 4 | .PP 5 | Paragraph 6 | of 7 | Text 8 | -------------------------------------------------------------------------------- /tests/simple.1.html: -------------------------------------------------------------------------------- 1 |

simple

HEADING

SUBHEADING

Paragraph of Text

2 | -------------------------------------------------------------------------------- /tests/soft_hyphen.1: -------------------------------------------------------------------------------- 1 | .TH man 1 2 | .SH H 3 | .ie c \[shc] \ 4 | . ds softhyphen \[shc] 5 | .el \ 6 | . ds softhyphen \(hy 7 | A\*[softhyphen]B 8 | -------------------------------------------------------------------------------- /tests/soft_hyphen.1.html: -------------------------------------------------------------------------------- 1 |

man

H

A-B

2 | -------------------------------------------------------------------------------- /tests/sox.1: -------------------------------------------------------------------------------- 1 | .TH sox 2 | .ds RA \(-> 3 | The overall SoX processing chain can be summarised as follows: 4 | .TS 5 | center; 6 | l. 7 | Input(s) \*(RA Combiner \*(RA Effects \*(RA Output(s) 8 | .TE 9 | -------------------------------------------------------------------------------- /tests/sox.1.html: -------------------------------------------------------------------------------- 1 |

sox

The overall SoX processing chain can be summarised as follows:

Input(s) → Combiner → Effects → Output(s)
2 | -------------------------------------------------------------------------------- /tests/sox.1b: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .if 1 .ds m " - 3 | A\*mB 4 | -------------------------------------------------------------------------------- /tests/sox.1b.html: -------------------------------------------------------------------------------- 1 |

A

A - B

2 | -------------------------------------------------------------------------------- /tests/spaces.1: -------------------------------------------------------------------------------- 1 | .TH spaces 1 2 | .SH HEADING 3 | .nf 4 | .B " hey" 5 | .fi 6 | -------------------------------------------------------------------------------- /tests/spaces.1.html: -------------------------------------------------------------------------------- 1 |

spaces

HEADING

   hey
2 | -------------------------------------------------------------------------------- /tests/tcpdump_rses.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .TP 4 | .B \-l 5 | Make stdout line buffered. 6 | .IP 7 | .RS 8 | .RS 9 | .nf 10 | \fBtcpdump \-l | tee dat\fP 11 | .fi 12 | .RE 13 | .RE 14 | .IP 15 | or 16 | .IP 17 | .RS 18 | .RS 19 | .nf 20 | \fBtcpdump \-l > dat & tail \-f dat\fP 21 | .fi 22 | .RE 23 | .RE 24 | .IP 25 | Note that on Windows 26 | -------------------------------------------------------------------------------- /tests/tcpdump_rses.1.html: -------------------------------------------------------------------------------- 1 |

A

H

-l

Make stdout line buffered.

tcpdump -l | tee dat

or

tcpdump -l > dat & tail -f dat

Note that on Windows

2 | -------------------------------------------------------------------------------- /tests/ti.1: -------------------------------------------------------------------------------- 1 | .TH Hey 1 1 2 | .SH HEADING 3 | A 4 | .ti 1 5 | B 6 | C 7 | .P 8 | D 9 | -------------------------------------------------------------------------------- /tests/ti.1.html: -------------------------------------------------------------------------------- 1 |

Hey

HEADING

A

B C

D

2 | -------------------------------------------------------------------------------- /tests/tp.1: -------------------------------------------------------------------------------- 1 | .TH tp 2 | .TP 3 | term 4 | definition 5 | -------------------------------------------------------------------------------- /tests/tp.1.html: -------------------------------------------------------------------------------- 1 |

tp

term

definition

2 | -------------------------------------------------------------------------------- /tests/tpthennewline.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH HEADING 3 | .TP 4 | 5 | .B A 6 | B 7 | -------------------------------------------------------------------------------- /tests/tpthennewline.1.html: -------------------------------------------------------------------------------- 1 |

A

HEADING

A

B

2 | -------------------------------------------------------------------------------- /tests/tr.1: -------------------------------------------------------------------------------- 1 | .TH tr 2 | .SH A 3 | .tr Q" 4 | .ds ms backup [QQ] 5 | .PP 6 | .B "\*(ms" 7 | .tr QQ 8 | -------------------------------------------------------------------------------- /tests/tr.1.html: -------------------------------------------------------------------------------- 1 |

tr

A

backup [""]

2 | -------------------------------------------------------------------------------- /tests/trailingTP.1: -------------------------------------------------------------------------------- 1 | .TH trailingTP 2 | .SH OPTIONS 3 | .TP 5 4 | .B A 5 | B 6 | .TP 5 7 | New paragraph 8 | .SH FILES 9 | -------------------------------------------------------------------------------- /tests/trailingTP.1.html: -------------------------------------------------------------------------------- 1 |

trailingTP

OPTIONS

A

B

New paragraph

FILES

2 | -------------------------------------------------------------------------------- /tests/ttylink.1: -------------------------------------------------------------------------------- 1 | .TH ttylink 2 | Something \X'tty: link http://example.com/'\fI\%blah\fP\X'tty: link' else 3 | -------------------------------------------------------------------------------- /tests/ttylink.1.html: -------------------------------------------------------------------------------- 1 |

ttylink

Something blah else

2 | -------------------------------------------------------------------------------- /tests/units.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .PP 4 | .nr a 1n 5 | a register is \na 6 | .PP 7 | .nr b 1i 8 | b register is \nb 9 | .PP 10 | .nr c 1in 11 | c register is \nc 12 | .PP 13 | .nr d \nc 14 | d register is \nd 15 | -------------------------------------------------------------------------------- /tests/units.1.html: -------------------------------------------------------------------------------- 1 |

A

H

a register is 21

b register is 240

c register is 240

d register is 240

2 | -------------------------------------------------------------------------------- /tests/url.1: -------------------------------------------------------------------------------- 1 | .de URL 2 | \\$2 \(la \\$1 \(ra\\$3 3 | .. 4 | .TH URL 5 | .P 6 | URL: 7 | .URL "http://www.example.com/" 8 | .P 9 | UR/UE: 10 | .UR https://example.com/something 11 | Some Project 12 | .UE . -------------------------------------------------------------------------------- /tests/url.1.html: -------------------------------------------------------------------------------- 1 |

URL

URL: http://www.example.com/

UR/UE: Some Project.

2 | -------------------------------------------------------------------------------- /tests/warnquota.conf.5: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH EXAMPLE 3 | .PP 4 | # comment 5 | .RS 0 6 | FROM = root@example.com 7 | .RS 0 8 | SUPPORT = support@example.com 9 | .RS 0 10 | PHONE = 1234 11 | .SH FILES 12 | -------------------------------------------------------------------------------- /tests/warnquota.conf.5.html: -------------------------------------------------------------------------------- 1 |

A

EXAMPLE

# comment
FROM = root@example.com
SUPPORT = support@example.com
PHONE = 1234

FILES

2 | -------------------------------------------------------------------------------- /tests/while.1: -------------------------------------------------------------------------------- 1 | .TH while 2 | .de XB 3 | . while \\n[.$] \{\ 4 | \$1 5 | . shift 6 | . \} 7 | .. 8 | .XB a b c d 9 | -------------------------------------------------------------------------------- /tests/while.1.html: -------------------------------------------------------------------------------- 1 |

while

a b c d

2 | -------------------------------------------------------------------------------- /tests/xpaenv.n: -------------------------------------------------------------------------------- 1 | .TH xpaenv n 2 | .SH Description 3 | .IP "\(bu" 4 4 | \&\fB\s-1XPA_ACL\s0\fR 5 | .sp 6 | If something something \fI\s-1XPA_ACL\s0\fR is \fItrue\fR, then 7 | .IP "\(bu" 4 8 | \&\fB\s-1XPA_ACLFILE\s0\fR 9 | .sp 10 | If something else 11 | -------------------------------------------------------------------------------- /tests/xpaenv.n.html: -------------------------------------------------------------------------------- 1 |

xpaenv

Description

  • XPA_ACL

    If something something XPA_ACL is true, then

  • XPA_ACLFILE

    If something else

2 | -------------------------------------------------------------------------------- /tests/xterm.1: -------------------------------------------------------------------------------- 1 | .TH xterm 1 2 | .de bP 3 | .ie n .IP \(bu 4 4 | .el .IP \(bu 2 5 | .. 6 | .bP 7 | hey 8 | -------------------------------------------------------------------------------- /tests/xterm.1.html: -------------------------------------------------------------------------------- 1 |

xterm

  • hey
2 | -------------------------------------------------------------------------------- /tests/zct.1: -------------------------------------------------------------------------------- 1 | .TH A 1 2 | .SH H 3 | .TS 4 | tab(:); 5 | c s s s c s s s, n n n nw4 n n n n. 6 | inp:out 7 | 1:1:2:2:4:4:3:3 8 | 1:1:2:2:4:4:3:3 9 | 3:3:4:4:2:2:1:1 10 | 3:3:4:4:2:2:1:1 11 | .TE 12 | -------------------------------------------------------------------------------- /tests/zct.1.html: -------------------------------------------------------------------------------- 1 |

A

H

inpout
11224433
11224433
33442211
33442211
2 | --------------------------------------------------------------------------------