├── CHANGELOG.textile
├── LICENSE
├── README.textile
├── SECURITY.md
├── composer.json
└── src
└── Netcarver
└── Textile
├── DataBag.php
├── Parser.php
└── Tag.php
/CHANGELOG.textile:
--------------------------------------------------------------------------------
1 | h1. Changelog
2 |
3 | Here's a summary of changes in each release. The list doesn't include some small changes or updates to test cases.
4 |
5 | h2. Version 4.1.4 - upcoming
6 |
7 | h2. "Version 4.1.3 - 2025/01/07":https://github.com/textile/php-textile/releases/tag/v4.1.3
8 |
9 | * This is a security update, which fixes stored XSS vulnerability in image link handing.
10 | * Fixed: In restricted mode, restrict image link protocol. Previously and an image link's href allowed all protocols in restricted mode. Now it goes through the same validation as text links.
11 |
12 | h2. "Version 4.1.2 - 2024/08/29":https://github.com/textile/php-textile/releases/tag/v4.1.2
13 |
14 | * Fix PHP 8.4 compatibility issues (closes "#227":https://github.com/textile/php-textile/issues/227).
15 |
16 | h2. "Version 4.1.1 - 2024/06/07":https://github.com/textile/php-textile/releases/tag/v4.1.1
17 |
18 | * Links are now rendered when block tags are disabled (closes "#225":https://github.com/textile/php-textile/issues/225).
19 |
20 | h2. "Version 4.1.0 - 2024/01/02":https://github.com/textile/php-textile/releases/tag/v4.1.0
21 |
22 | * Support @:@ and @!@ characters in CSS class names (closes "#224":https://github.com/textile/php-textile/issues/224).
23 | * Support including textile escape sequences inside link title attribute (closes "#223":https://github.com/textile/php-textile/issues/223).
24 |
25 | h2. "Version 4.0.0 - 2022/12/03":https://github.com/textile/php-textile/releases/tag/v4.0.0
26 |
27 | * HTML void tags render a self-closing slash based on the given document type. If @Parser::setDocumentType()@ is given @Parser::DOCTYPE_XHTML@, self-closing tags are used, otherwise not.
28 |
29 | h2. "Version 3.8.0 - 2022/12/03":https://github.com/textile/php-textile/releases/tag/v3.8.0
30 |
31 | * Added @Parser::setAlignClasses()@ and @Parser::isAlignClassesEnabled()@. This can be used to enable img alignment classes in XHTML output document mode, instead of the default align attribute.
32 | * Added @Parser::DOCTYPE_HTML5@ and @Parser::DOCTYPE_XHTML@ constants. These can be used with @Parser::setDocumentType()@ to specify the output document type.
33 |
34 | h2. "Version 3.7.7 - 2022/05/01":https://github.com/textile/php-textile/releases/tag/v3.7.7
35 |
36 | * Fix deprecation errors that appear on PHP >= 8.1 about preg_split limit argument's NULL value.
37 |
38 | h2. "Version 3.7.6 - 2020/01/08":https://github.com/textile/php-textile/releases/tag/v3.7.6
39 |
40 | * Support consecutive links without whitespace between using bracket syntax (closes "#202":https://github.com/textile/php-textile/issues/202, "#205":https://github.com/textile/php-textile/pull/205 and "#206":https://github.com/textile/php-textile/pull/206).
41 |
42 | h2. "Version 3.7.5 - 2019/12/16":https://github.com/textile/php-textile/releases/tag/v3.7.5
43 |
44 | * Fix PHP 7.4 compatibility issues (closes "#199":https://github.com/textile/php-textile/issues/199).
45 |
46 | h2. "Version 3.7.4 - 2019/12/15":https://github.com/textile/php-textile/releases/tag/v3.7.4
47 |
48 | * Fix issue where an inline tag preceding the last character, that is a glyph, is not rendered if the block tags are disabled with @Parser::setBlockTags@ (closes "#198":https://github.com/textile/php-textile/issues/198).
49 |
50 | h2. "Version 3.7.3 - 2019/08/30":https://github.com/textile/php-textile/releases/tag/v3.7.3
51 |
52 | * Fix issues where divider tags placed on their own line within a paragraph, would disable Textile processing for that paragraph block (closes "#194":https://github.com/textile/php-textile/issues/194).
53 |
54 | h2. "Version 3.7.2 - 2019/06/08":https://github.com/textile/php-textile/releases/tag/v3.7.2
55 |
56 | * Fix quote and bracket processing around span and other inline tags (closes "#191":https://github.com/textile/php-textile/issues/191 and "#192":https://github.com/textile/php-textile/issues/192).
57 |
58 | h2. "Version 3.7.1 - 2019/01/26":https://github.com/textile/php-textile/releases/tag/v3.7.1
59 |
60 | * Fix and omit anchor links prefixing (closes "#190":https://github.com/textile/php-textile/issues/190).
61 | * Alignment attribute can be specified last within a block tag definition (closes "#189":https://github.com/textile/php-textile/issues/190)
62 |
63 | h2. "Version 3.7.0 - 2018/12/15":https://github.com/textile/php-textile/releases/tag/v3.7.0
64 |
65 | * Fix problems with list parsing; no longer matches inline-syntax, such as strongs, as list item markers (closes "#172":https://github.com/textile/php-textile/issues/172).
66 | * Add option to disable specific substitution symbols by setting them to FALSE (see "#158":https://github.com/textile/php-textile/issues/158).
67 | * Add option to apply classes, such as language-identifiers, to the code element within a @bc@ (closes "#96":https://github.com/textile/php-textile/issues/96).
68 | * Add @Parser::configure()@, the method can be extended to create pre-configured parser classes.
69 | * Automatic paragraph wrapping now checks the contents and does not wrap paragraphs already wrapped in non-phrasing HTML tags (closes "#22":https://github.com/textile/php-textile/issues/22 and "#63":https://github.com/textile/php-textile/issues/63).
70 | * Add option to disable Textile formatting for blocks wrapped in non-standard HTML-like tags.
71 | * Check for starting list depth (closes "#24":https://github.com/textile/php-textile/issues/24).
72 | * Add @Parser::setImagePrefix()@, @Parser::setLinkPrefix()@, @Parser::getImagePrefix()@ and @Parser::getLinkPrefix()@ (closes "#169":https://github.com/textile/php-textile/issues/169).
73 | * Add @Parser::setRawBlocks()@ and @Parser::isRawBlocksEnabled()@.
74 | * Deprecate @Parser::setRelativeImagePrefix()@ and @Parser::$relativeImagePrefix@ in favour of the new decoupled methods.
75 |
76 | h2. "Version 3.6.1 - 2018/10/21":https://github.com/textile/php-textile/releases/tag/v3.6.1
77 |
78 | * Fix problems with attribute parsing under PHP >= 7.1 (closes "#175":https://github.com/textile/php-textile/issues/175 and "#176":https://github.com/textile/php-textile/issues/176).
79 | * Fix test suite compatibility issues under PHP >= 7.2 (closes "#184":https://github.com/textile/php-textile/issues/184).
80 | * Fix missing deprecation notices.
81 | * Add test fixture for Unicode characters in image title attributes.
82 | * Tidy entity encoding process (closes "#182":https://github.com/textile/php-textile/issues/182).
83 | * Drop legacy PHP 5.5 and 5.4 unit test targets due to test suite's depedencies' requirements.
84 |
85 | h2. "Version 3.6.0 - 2016/11/17":https://github.com/textile/php-textile/releases/tag/v3.6.0
86 |
87 | * Fix empty-like link texts (closes "#141":https://github.com/textile/php-textile/issues/141).
88 | * Fix empty-like RedCloth definitions (closes "#142":https://github.com/textile/php-textile/issues/142).
89 | * Fix empty-like table summaries (closes "#143":https://github.com/textile/php-textile/issues/143).
90 | * Fix image dimension generation on Windows when doc_root can not be resolved (closes "#140":https://github.com/textile/php-textile/issues/140).
91 | * Fix HTTP protocol restrictions not affecting images (closes "#144":https://github.com/textile/php-textile/issues/144).
92 | * Add more versatile parsing method, @Parser::parse()@.
93 | * Add @Parser::setRestricted()@ and @Parser::isRestrictedModeEnabled()@.
94 | * Add @Parser::setLite()@ and @Parser::isLiteModeEnabled()@.
95 | * Add @Parser::setDocumentType()@ and @Parser::getDocumentType()@.
96 | * Add @Parser::setDocumentRootDirectory()@ and @Parser::getDocumentRootDirectory()@.
97 | * Add @Parser::setImages()@ and @Parser::isImageTagEnabled()@.
98 | * Add @Parser::setBlockTags()@ and @Parser::isBlockTagEnabled()@ (closes "#138":https://github.com/textile/php-textile/issues/138).
99 | * Add @Parser::setLinkRelationShip()@ and @Parser::getLinkRelationShip()@.
100 | * Add @Parser::setLineWrap()@ and @Parser::isLineWrapEnabled()@ (closes "#139":https://github.com/textile/php-textile/issues/139).
101 | * Deprecate @Parser::textileThis()@, @Parser::textileRestricted()@ and @Parser::textileCommon()@ in favour of the more versatile @Parser::parse()@.
102 | * Fix: @Parser::relURL()@ now supports unicode characters (closes "#146":https://github.com/textile/php-textile/issues/146).
103 | * Fix: Undefined variable warning.
104 | * Feature/Fix: Allow link text that contains newline characters (closes "#154":https://github.com/textile/php-textile/issues/154, "#155":https://github.com/textile/php-textile/issues/155 & "#167":https://github.com/textile/php-textile/issues/167).
105 | * Fix: Stop encoding '+' characters in tel: links (closes "#156":https://github.com/textile/php-textile/issues/156).
106 | * Make dimension glyph replacements a little stricter.
107 | * Fix: Prevent hyphenated class on td cells adding incorrect style (closes "#164":https://github.com/textile/php-textile/issues/164).
108 | * Jail read of image dimensions to images within the document root path (closes "#145":https://github.com/textile/php-textile/issues/145).
109 | * Various code cleanups, typo corrections and refactoring.
110 | * Documentation fixes and extensions.
111 |
112 | h2. "Version 3.5.5 - 2014/01/02":https://github.com/textile/php-textile/releases/tag/v3.5.5
113 |
114 | * Fix rendering of left and right image alignment in non-lite restricted mode (closes "#132":https://github.com/textile/php-textile/issues/132).
115 | * Fix wrong triggered error type when using the deprecated @$encode@ option of @Parser::textileThis()@.
116 | * Fix attribute regular expression to stop it matching multiple times (closes "#131":https://github.com/textile/php-textile/issues/131).
117 | * Fix rendering of lists in table cells with span attributes set (closes "#135":https://github.com/textile/php-textile/issues/135).
118 | * Throws an exception if @Parser::__construct()@ is given invalid document type, instead of eating it silently and returning document using the default content-type you weren't wishing for. Prevents issues where you want HTML5, but silently got XHTML due to typo or an issue in application design.
119 | * Clean user-supplied styles prior to sorting and re-formatting.
120 | * Remove dead code and duplicated procedures as outlined by code coverage reports.
121 | * Remove unused internal method @Parser::fSpecial()@.
122 | * Test code coverage, coding style and run unit tests against "HHVM":http://hhvm.com.
123 | * Footnote reference numbers support unicode characters.
124 | * Use named sub-patterns in regular expressions (closes "#121":https://github.com/textile/php-textile/issues/121).
125 | * Refactored link detection code.
126 |
127 | h2. "Version 3.5.4 - 2013/11/06":https://github.com/textile/php-textile/releases/tag/v3.5.4
128 |
129 | * Fix broken image alignment in HTML5 mode (closes "#123":https://github.com/textile/php-textile/issues/123).
130 | * Fix duplicate HTML IDs that occur when a footnote isn't referenced in the content (closes "#125":https://github.com/textile/php-textile/issues/125).
131 | * Don't include image alignment to the URL in restricted mode.
132 | * Detect and process quoted quote symbols.
133 | * New link parser (closes "#86":https://github.com/textile/php-textile/issues/86, "#87":https://github.com/textile/php-textile/issues/87 and "#128":https://github.com/textile/php-textile/issues/128).
134 |
135 | h2. "Version 3.5.3 - 2013/10/30":https://github.com/textile/php-textile/releases/tag/v3.5.3
136 |
137 | * Fix double image URL encoding (closes "#102":https://github.com/textile/php-textile/issues/102).
138 | * Fix URL reference token spoofing.
139 | * Fix broken parser output when $strict argument was set to TRUE (closes "#119":https://github.com/textile/php-textile/issues/119).
140 | * Fix memory leaking tag cache. Tag cache is never reset between @textileThis()@ and @textileRestricted()@ calls referencing the same instance.
141 | * Fix rare instances where a link displays a wrong URL mentioned elsewhere in the document.
142 | * Fix invalid markup generated when Redcloth-style definition list is used inside a table cell.
143 | * Link aliases follow same allowed URL schemes as normal links.
144 | * Update @hasRawText()@ and @fPBr()@ to detect a wider range of raw HTML and XHTML.
145 | * Unify attribute order with Redcloth.
146 | * Reduce list and blockquote indentation level to match paragraphs and other block tags.
147 | * Restrict how spans are parsed (closes "#106":https://github.com/textile/php-textile/issues/106).
148 | * Fix citations on spans (closes "#120":https://github.com/textile/php-textile/issues/120).
149 | * Refactor @parseAttribsToArray()@ slightly.
150 |
151 | h2. "Version 3.5.2 - 2013/10/25":https://github.com/textile/php-textile/releases/tag/v3.5.2
152 |
153 | * Improved support for Redcloth-style definition lists.
154 | *# Allow multiple terms
155 | *# Allow linebreaks in terms
156 | * Fix incorrectly rendered @rel@ attributes (closes "#103":https://github.com/textile/php-textile/issues/103).
157 | * Fix @getSymbol()@ so it actually returns the named symbol (closes "#104":https://github.com/textile/php-textile/issues/104).
158 | * Fix unicode link aliases that were broken on some PCRE_UTF8 supporting systems.
159 | * Fix collapsing whitespace and preserve newlines. Preserves whitespace inside long @bc@, @notextile@ and @pre@ blocks, rather than collapsing two or more empty lines down to one. Renders whitespace as it was defined, rather than using hard-coded single LF to separate lines (closes "#109":https://github.com/textile/php-textile/issues/109 and "#111":https://github.com/textile/php-textile/pull/111).
160 | * Fix the number of code tags rendered inside long code blocks (closes "#116":https://github.com/textile/php-textile/issues/116).
161 | * Fix token spoofing from the document body by randomizing token references (closes "#115":https://github.com/textile/php-textile/issues/115).
162 | * Add image dimensions to images even when Textile is run on command line. On CLI, images are looked from the current working directory.
163 | * Define internal class properties as protected rather than at all, causing them to be created as public.
164 | * Move internal property definitions from the constructor to class definition.
165 | * Added runnable PHPUnit tests, integration with "Travis CI":https://travis-ci.org/.
166 | * Removed error suppression, the code doesn't intentionally produce notices.
167 | * Add @br@ tags to headings instead of leaving linebreaks untouched.
168 |
169 | h2. "Version 3.5.1 - 2013/01/01":https://github.com/textile/php-textile/releases/tag/v3.5.1
170 |
171 | * Remove horizontal alignment from inline elements (closes "#66":https://github.com/textile/php-textile/issues/66).
172 | * Reinstate automatic generation of image width and height generation for relative images (closes "#101":https://github.com/textile/php-textile/issues/101).
173 | * Add @setDimensionlessImages()@ and @getDimensionlessImages()@ to suppress width and height generation for relative images and better support content for responsive layouts (closes "#100":https://github.com/textile/php-textile/issues/101).
174 | * Allow "." in class attributes (closes "#97":https://github.com/textile/php-textile/issues/97).
175 |
176 | h2. "Version 3.5.0 - 2012/12/12":https://github.com/textile/php-textile/releases/tag/v3.5.0
177 |
178 | * Add composer.json to allow installation via the "Composer PHP package manager":https://getcomposer.org/.
179 | * Regular expression improvements (issues "#78":https://github.com/textile/php-textile/issues/78, "#81":https://github.com/textile/php-textile/issues/81 and "#83":https://github.com/textile/php-textile/issues/83).
180 | * Allow pre-encoded @>@ and @<@ as alignments.
181 | * Self-referencing links can now be combined with link aliases. eg. @"$":alias1@ is now possible.
182 | * Fix memory leak and performance degradation when calling same Textile object multiple times.
183 | * Do not double-encode @+@ or @%@ in urls.
184 | * Remove legacy SVN lines and old Textpattern integration methods.
185 | * Refactored code, removing deprecated methods.
186 | * Fix undefined variable in Redcloth-style definition lists.
187 | * Improvements to image handling (closes "#69":https://github.com/textile/php-textile/issues/69).
188 | * Extend recognition of dimension sign to more complex cases. Eg...
189 | *# @-0.5 x -.1 x +100@ => %-0.5 × -.1 × +100%
190 | *# @10 x -€ 110,00@ => %10 × -€ 110,00% (this replacement is available only if unicode support is included in your PCRE implementation and any character that is a currency symbol should work)
191 | * Change parse tokens to further prevent glyphs from matching them internally.
192 | * Improve handling of textile within table cells without leading or trailing spaces (eg. @|"$":https://github.com"|_Here we are_|==code==|@ etc).
193 | * Improve handling of lists within table cells. (closes "#79":https://github.com/textile/php-textile/issues/79)
194 | * Allow mixed nested lists (Already supported in Redcloth).
195 | * Improve detection of open quotes in situations like @["(Berk). Hilton"]@ (where the open quote was previously was incorrectly detected encoded).
196 | * Fix a problem with links followed by ':', ';' or '?' like @Do you like "cheese":/cheese?@ where the '?' becomes part of the href rather than a '?' at the end of the sentence. This happens on platforms where PCRE has unicode support.
197 | * Removal of leading \t from generated paragraphs & better indentation of generated lists in the HTML (closes "#90":https://github.com/textile/php-textile/issues/90).
198 | * New method @textileEncode()@. This is preferred to calling @textileThis()@ with the $encode flag.
199 | * "PSR-0, PSR-1 & PSR-2":https://github.com/php-fig/fig-standards/tree/master/accepted conformity added.
200 | *# Split class Textile into classes @Parser@, @DataBag@ & @Tag@ and moved them into @src/textile/php-textile@ directory for PSR-0.
201 | *# Unified method names & coding styles according to PSR-1 & PSR-2.
202 | *# Switched from using defines to explicit set methods; @setSymbol()@ & @setRelativeImagePrefix()@.
203 | *# Added visibility controls to all properties and methods.
204 | * Dropped textile's PHP4 heritage and moved over to using a @__construct()@ method.
205 |
206 | h2. "Version 2.4.1 - 2012/08/23":https://github.com/textile/php-textile/releases/tag/v2.4.1
207 |
208 | * Add @sftp@, @callto@, @tel@ and @file@ schemes to the URI whitelist in unrestricted mode.
209 | * Support international format @tel@ URIs via linkrefs (link aliases).
210 | * Extend linkrefs to all available URI schemes in unrestricted mode.
211 | * Reverted looser matching of list-like structures as it introduced problems in block-level elements that
212 | can legitimately have literals or other non-lists in them that are similar to textile's lists.
213 | See issue "#65"::https://github.com/textile/php-textile/issues/65 for some examples.
214 | * Fixed "#61"::https://github.com/textile/php-textile/issues/61 which caused strong numeric strings at the start of a line to be turned into lists.
215 | * Reverted auto-breaking in table cells as it introduced "#71"::https://github.com/textile/php-textile/issues/71 (breaking lists within table cells).
216 | * Fixed "#67"::https://github.com/textile/php-textile/issues/67 which ran textile within table cells through the glyph routine twice.
217 |
218 | h2. "Version 2.4.0 - 2012/05/07":https://github.com/textile/php-textile/releases/tag/v2.4.0
219 |
220 | * Conditionally use utf8 in span regex pattern. Closes "#53"::https://github.com/textile/php-textile/issues/53.
221 | * Small code cleanups
222 | * Convert \n to @ @ inside table rows.
223 | * Added HTML comment block handling.
224 | * Added ability to control the start attribute and continuation of ordered lists.
225 | * Adds basic Redcloth-style definition list support.
226 | * Add ability to customise footnote refs and anchors.
227 | * Add https protocol to linkrefs.
228 | * Add unicode support for linkref urls.
229 | * Added redcloth style list continuation.
230 | * Loosen recognition of notedefs and notelists.
231 | * Better recognition of lists adjoined to previous text. (This was reverted in 2.4.1 as it caused issues inside block elements like bc. or bq).
232 | * Bugfix: allow apostrophe between ) and a word character.
233 | * Allow notelist refs to be customised.
234 |
235 | h2. "Version 2.3.2 - 2012/03/20":https://github.com/textile/php-textile/releases/tag/v2.3.2
236 |
237 | * Allow multiple classes in block attributes.
238 | * Improve handling of (classA classB#Bad id) blocks.
239 | * Add span $tail fix & support for trailing [ in spans.
240 | * Add doctype to textile constructor & use abbr instead of acronym for html5 doctypes.
241 | * HTML5 doctype extends class rather than use invalid align attribute.
242 | * Allow auto-assigned classes to be output in restricted mode.
243 | * Add support for encoding Unicode characters in links.
244 | * Notelists: fix order of @sup@ and @a@. Fixes issue "#20"::https://github.com/textile/php-textile/issues/20
245 | * Add simple set of symbols ¤§µ¶†‡•∗∴◊♠♣♥♦ to notelist regex. Requested in "#38"::https://github.com/textile/php-textile/issues/38
246 | * Remove attributes from embedded code tag output by bc.
247 | * Simplify en-dash glyph rule -- should fix "#50"::https://github.com/textile/php-textile/issues/50.
248 | * Simplify self-hyperlinked text, remove the scheme for http, https, ftp and mailto link text.
249 |
250 | h2. "Version 2.3.0 - 2012/01/10":https://github.com/textile/php-textile/releases/tag/v2.3
251 |
252 | * Fix potential DoS in @cleanba()@.
253 | * Fix the issue where class would eat the note label.
254 | * Sanitise image URLs.
255 | * Allow inline span tags to be applied within non-English quotation marks.
256 | * Allow non-English quotation marks inside inline span tags.
257 | * Allow pipe closure of captions.
258 | * Allow missing closing pipe in colgroups.
259 | * Note-style links can use index 0.
260 | * Encode quotes in restricted mode, rather than improperly leaving them as is.
261 | * Improve lang, style, id and class handling.
262 | * Add rel attributes to linked images.
263 | * Center aligned cells aren't treated as captions.
264 | * Removed use of deprecated @split()@ function.
265 | * Disallow unsafe block attributes in restricted mode.
266 |
267 | h2. "Version 2.2.0 - 2010/09/22":https://github.com/textile/php-textile/releases/tag/v2.2public
268 |
269 | This is our first release forked from "revision 3359":https://code.google.com/p/textpattern/source/detail?r=3359 of "Textpattern CMS":http://textpattern.com. Here are the changes since Textile v2.0.0:
270 |
271 | * Allow duplicate notelists with different backref characters.
272 | * Properly render empty table cells.
273 | * Add support for glyphs such as fractions, plus-minus and degrees.
274 | * Optimize glyph encoding setup.
275 | * Optimize style attribute processing.
276 | * Less restrictive paragraph breaking.
277 | * Output cleaner inline styles.
278 | * More restrictive apostrophe encoding and matching.
279 | * Remove horizontal and vertical alignment attributes from list elements.
280 | * Lists can use dot terminator.
281 | * Improved table generation: allow linebreaks in table cells, colgroups, thead, tbody and tfoot elements.
282 | * Add auto-numbered notelists.
283 | * Add Textile comment tag. Comments aren't displayed in the generated markup.
284 | * Add self-links where the URL can be used with the link text (@"$":http://example.com@).
285 | * Allow glyph parsing across tag boundaries.
286 | * Add definition lists.
287 | * Fix duplicate IDs in footnotes.
288 | * Caps span isn't added to acronyms.
289 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2003-2004, Dean Allen
4 | All rights reserved.
5 |
6 | Thanks to Carlo Zottmann for refactoring Textile's procedural
7 | code into a class framework.
8 |
9 | Additions and fixes Copyright (c) 2006 Alex Shiels https://twitter.com/tellyworth
10 | Additions and fixes Copyright (c) 2010 Stef Dawson https://stefdawson.com/
11 | Additions and fixes Copyright (c) 2010-17 Netcarver https://github.com/netcarver
12 | Additions and fixes Copyright (c) 2011 Jeff Soo http://ipsedixit.net/
13 | Additions and fixes Copyright (c) 2012 Robert Wetzlmayr https://wetzlmayr.com/
14 | Additions and fixes Copyright (c) 2012-24 Jukka Svahn https://rahforum.biz/
15 |
16 | Redistribution and use in source and binary forms, with or without
17 | modification, are permitted provided that the following conditions are met:
18 |
19 | 1. Redistributions of source code must retain the above copyright notice, this
20 | list of conditions and the following disclaimer.
21 |
22 | 2. Redistributions in binary form must reproduce the above copyright notice,
23 | this list of conditions and the following disclaimer in the documentation
24 | and/or other materials provided with the distribution.
25 |
26 | 3. Neither the name of the Textile nor the names of its
27 | contributors may be used to endorse or promote products derived from
28 | this software without specific prior written permission.
29 |
30 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
31 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
32 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
33 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
34 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
35 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
36 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
37 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
38 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
39 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40 |
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | h1. PHP-Textile
2 |
3 | "Textile reference":https://textile-lang.com/ | "Live editor":https://textile-lang.com/doc
4 |
5 | !https://sonarcloud.io/api/project_badges/measure?project=textile_php-textile&metric=coverage!:https://sonarcloud.io/summary/overall?id=textile_php-textile !https://img.shields.io/packagist/v/netcarver/textile.svg!:https://packagist.org/packages/netcarver/textile !https://img.shields.io/packagist/dt/netcarver/textile.svg!:https://packagist.org/packages/netcarver/textile
6 |
7 | PHP-Textile is a modern Textile markup language parser for PHP. Textile is a humane web text generator that takes lightweight, readable, plaintext-like markup language and converts it into well formed HTML.
8 |
9 | h2. Install
10 |
11 | Using "Composer":https://getcomposer.org/:
12 |
13 | bc. $ composer require netcarver/textile
14 |
15 | h2. Usage
16 |
17 | The Textile parser can be accessed through the @Netcarver\Textile\Parser@ class. The class is highly configurable, and actual parsing happens with the @parse@ method:
18 |
19 | bc. require './vendor/autoload.php';
20 | $parser = new \Netcarver\Textile\Parser();
21 | echo $parser->parse('h1. Hello World!');
22 |
23 | h3. Parsing untrusted input
24 |
25 | If you are using PHP-Textile to format user-supplied input, blog comments for instance, remember to enable restricted parser mode:
26 |
27 | bc. $parser = new \Netcarver\Textile\Parser();
28 | echo $parser
29 | ->setRestricted(true)
30 | ->parse('!bad/image/not/allowed.svg!');
31 |
32 | In restricted mode PHP-Textile doesn't allow more powerful formatting options such as inline styles, and removes any raw HTML.
33 |
34 | h3. Parsing single-line fields
35 |
36 | If you are using PHP-Textile as a field-level formatter to parse just inline spans and glyphs, use the @setBlockTags@ method to disable block tags:
37 |
38 | bc. $parser = new \Netcarver\Textile\Parser();
39 | echo $parser
40 | ->setBlockTags(false)
41 | ->parse('Hello *strong* world!');
42 |
43 | The above outputs:
44 |
45 | bc. Hello strong world!
46 |
47 | h3. Doctypes
48 |
49 | Currently, PHP-Textile can target either XHTML or HTML5 output with XHTML being the default for backward compatibility. The targeted doctype can be changed via the @setDocumentType@ method:
50 |
51 | bc. $parser = new \Netcarver\Textile\Parser();
52 | echo $parser
53 | ->setDocumentType('html5')
54 | ->parse('HTML(HyperText Markup Language)');
55 |
56 | h3. Setting alternate glyphs
57 |
58 | Textile's typographic substitutions can be overridden with the @setSymbol@ method. If you need to setup Textile to do non-standard substitutions, call @setSymbol@ before you parse the input with @parse@.
59 |
60 | bc. $parser = new \Netcarver\Textile\Parser();
61 | $parser
62 | ->setSymbol('half', '1⁄2')
63 | ->parse('Hello [1/2] World!');
64 |
65 | The symbol names you can pass to @setSymbol@ can be found "here":https://github.com/textile/php-textile/blob/v3.6.1/src/Netcarver/Textile/Parser.php#L710.
66 |
67 | h3. Prefixing relative image and link paths
68 |
69 | Setting prefix might be useful if you want to point relative paths to certain consistent location:
70 |
71 | bc. $parser = new \Netcarver\Textile\Parser();
72 | $parser
73 | ->setImagePrefix('/user/uploads')
74 | ->setLinkPrefix('/')
75 | ->parse('!image.jpg! "link":page');
76 |
77 | h2. Getting in contact
78 |
79 | The PHP-Textile project welcomes constructive input and bug reports from users. Please "open an issue":https://github.com/textile/php-textile/issues on the repository for a comment, feature request or bug.
80 |
81 | h2. Development
82 |
83 | See "CONTRIBUTING.textile":https://github.com/textile/php-textile/blob/master/.github/CONTRIBUTING.textile.
84 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | You can privately report security vulnerabilities to PHP-Textile team by opening a
6 | [new draft security advisory](https://github.com/textile/php-textile/security/advisories/new)
7 | to us on GitHub.
8 |
9 | When opening a new advisory, take the following considerations into account:
10 |
11 | * Before opening a security advisory, please try to confirm that the security
12 | issue is caused by PHP-Textile, and not by third-party or configuration
13 | error.
14 | * Provide details as to the nature of the vulnerability, and examples of the steps to
15 | replicate it.
16 | * PHP-Textile is a free, open-source project run by volunteers, and we do not offer monetary
17 | rewards or provide bug bounties for discovering security issues.
18 | * Due to the volunteer-nature, our response times may not be immediate. We do kindly ask to allow
19 | us a reasonable amount of time to evaluate and correct the issue before making details public.
20 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "netcarver/textile",
3 | "description": "Textile markup language parser",
4 | "license": "BSD-3-Clause",
5 | "homepage": "https://github.com/textile/php-textile",
6 | "keywords": ["php-textile", "textile", "parser", "markup", "language", "html", "format", "plaintext", "document"],
7 | "support": {
8 | "wiki": "https://github.com/textile/php-textile/wiki",
9 | "issues": "https://github.com/textile/php-textile/issues",
10 | "source": "https://github.com/textile/php-textile"
11 | },
12 | "autoload": {
13 | "psr-4": {
14 | "Netcarver\\Textile\\": "src/Netcarver/Textile/"
15 | }
16 | },
17 | "autoload-dev": {
18 | "psr-4": {
19 | "Netcarver\\Textile\\Test\\": "test/Netcarver/Textile/Test/"
20 | }
21 | },
22 | "require": {
23 | "php": ">=5.3.0"
24 | },
25 | "require-dev": {
26 | "phpstan/phpstan": "1.12.11",
27 | "phpunit/phpunit": "^9.5.20",
28 | "squizlabs/php_codesniffer": "3.*",
29 | "symfony/yaml": "^5.4.40",
30 | "psy/psysh": "^0.12.4"
31 | },
32 | "extra": {
33 | "branch-alias": {
34 | "dev-master": "4.1-dev"
35 | }
36 | },
37 | "scripts": {
38 | "test": [
39 | "@composer lint",
40 | "@composer test:static",
41 | "@composer test:unit"
42 | ],
43 | "project:bump": "@php ./scripts/release.php",
44 | "project:bump-dev": "@php ./scripts/release.php --dev",
45 | "lint": "phpcs",
46 | "lint-fix": "phpcbf",
47 | "repl": "psysh",
48 | "test:static": "phpstan analyse --level 8 src",
49 | "test:unit": "XDEBUG_MODE=coverage phpunit"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Netcarver/Textile/DataBag.php:
--------------------------------------------------------------------------------
1 | 'value'));
49 | * $plant->flower('rose')->color('red');
50 | *
51 | * @internal
52 | */
53 | class DataBag
54 | {
55 | /**
56 | * The data array stored in the bag.
57 | *
58 | * @var array
59 | */
60 | protected $data;
61 |
62 | /**
63 | * Constructor.
64 | *
65 | * @param array|null $data The initial data array stored in the bag
66 | */
67 | public function __construct($data = null)
68 | {
69 | $this->data = (array) $data;
70 | }
71 |
72 | /**
73 | * Adds a value to the bag.
74 | *
75 | * Empty values are rejected, unless the
76 | * second argument is set TRUE.
77 | *
78 | * bc. use Netcarver\Textile\DataBag;
79 | * $plant = new DataBag(array('key' => 'value'));
80 | * $plant->flower('rose')->color('red')->emptyValue(false, true);
81 | *
82 | * @param string $name The name
83 | * @param array $params Arguments
84 | * @return DataBag
85 | */
86 | public function __call($name, array $params)
87 | {
88 | if (!empty($params[1]) || !empty($params[0])) {
89 | $this->data[$name] = $params[0];
90 | }
91 |
92 | return $this;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Netcarver/Textile/Parser.php:
--------------------------------------------------------------------------------
1 | for refactoring
16 | * Textile's procedural code into a class framework
17 | *
18 | * Additions and fixes Copyright (c) 2006 Alex Shiels https://twitter.com/tellyworth
19 | * Additions and fixes Copyright (c) 2010 Stef Dawson http://stefdawson.com/
20 | * Additions and fixes Copyright (c) 2010-17 Netcarver https://github.com/netcarver
21 | * Additions and fixes Copyright (c) 2011 Jeff Soo http://ipsedixit.net/
22 | * Additions and fixes Copyright (c) 2012 Robert Wetzlmayr http://wetzlmayr.com/
23 | * Additions and fixes Copyright (c) 2012-24 Jukka Svahn https://rahforum.biz/
24 | *
25 | * Redistribution and use in source and binary forms, with or without
26 | * modification, are permitted provided that the following conditions are met:
27 | *
28 | * * Redistributions of source code must retain the above copyright notice,
29 | * this list of conditions and the following disclaimer.
30 | *
31 | * * Redistributions in binary form must reproduce the above copyright notice,
32 | * this list of conditions and the following disclaimer in the documentation
33 | * and/or other materials provided with the distribution.
34 | *
35 | * * Neither the name Textile nor the names of its contributors may be used to
36 | * endorse or promote products derived from this software without specific
37 | * prior written permission.
38 | *
39 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
40 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
41 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
42 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
43 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
44 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
45 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
46 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
47 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
48 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
49 | * POSSIBILITY OF SUCH DAMAGE.
50 | */
51 |
52 | /*
53 | Textile usage examples.
54 |
55 | Block modifier syntax:
56 |
57 | Header: h(1-6).
58 | Paragraphs beginning with 'hn. ' (where n is 1-6) are wrapped in header tags.
59 | Example: h1. Header... -> Header...
60 |
61 | Paragraph: p. (also applied by default)
62 | Example: p. Text -> Text
63 |
64 | Blockquote: bq.
65 | Example: bq. Block quotation... -> Block quotation...
66 |
67 | Blockquote with citation: bq.:http://citation.url
68 | Example: bq.:http://example.com/ Text...
69 | -> Text...
70 |
71 | Footnote: fn(1-100).
72 | Example: fn1. Footnote... -> Footnote...
73 |
74 | Numeric list: #, ##
75 | Consecutive paragraphs beginning with # are wrapped in ordered list tags.
76 | Example: ordered list
77 |
78 | Bulleted list: *, **
79 | Consecutive paragraphs beginning with * are wrapped in unordered list tags.
80 | Example:
81 |
82 | Definition list:
83 | Terms ;, ;;
84 | Definitions :, ::
85 | Consecutive paragraphs beginning with ; or : are wrapped in definition list tags.
86 | Example: term definition
87 |
88 | Redcloth-style Definition list:
89 | - Term1 := Definition1
90 | - Term2 := Extended
91 | definition =:
92 |
93 | Phrase modifier syntax:
94 |
95 | _emphasis_ -> emphasis
96 | __italic__ -> italic
97 | *strong* -> strong
98 | **bold** -> bold
99 | ??citation?? -> citation
100 | -deleted text- -> deleted
101 | +inserted text+ -> inserted
102 | ^superscript^ -> superscript
103 | ~subscript~ -> subscript
104 | @code@ -> computer code
105 | %(bob)span% -> span
106 |
107 | ==notextile== -> leave text alone (do not format)
108 |
109 | "linktext":url -> linktext
110 | "linktext(title)":url -> linktext
111 | "$":url -> url
112 | "$(title)":url -> url
113 |
114 | !imageurl! ->
115 | !imageurl(alt text)! ->
116 | !imageurl!:linkurl ->
117 |
118 | ABC(Always Be Closing) -> ABC
119 |
120 | Linked Notes:
121 |
122 | Allows the generation of an automated list of notes with links.
123 |
124 | Linked notes are composed of three parts, a set of named _definitions_, a set of
125 | _references_ to those definitions and one or more _placeholders_ indicating where
126 | the consolidated list of notes is to be placed in your document.
127 |
128 | Definitions:
129 |
130 | Each note definition must occur in its own paragraph and should look like this...
131 |
132 | note#mynotelabel. Your definition text here.
133 |
134 | You are free to use whatever label you wish after the # as long as it is made up
135 | of letters, numbers, colon(:) or dash(-).
136 |
137 | References:
138 |
139 | Each note reference is marked in your text like this[#mynotelabel] and
140 | it will be replaced with a superscript reference that links into the list of
141 | note definitions.
142 |
143 | List placeholder(s):
144 |
145 | The note list can go anywhere in your document. You have to indicate where
146 | like this:
147 |
148 | notelist.
149 |
150 | notelist can take attributes (class#id) like this: notelist(class#id).
151 |
152 | By default, the note list will show each definition in the order that they
153 | are referenced in the text by the _references_. It will show each definition with
154 | a full list of backlinks to each reference. If you do not want this, you can choose
155 | to override the backlinks like this...
156 |
157 | notelist(class#id)!. Produces a list with no backlinks.
158 | notelist(class#id)^. Produces a list with only the first backlink.
159 |
160 | Should you wish to have a specific definition display backlinks differently to this
161 | then you can override the backlink method by appending a link override to the
162 | _definition_ you wish to customise.
163 |
164 | note#label. Uses the citelist's setting for backlinks.
165 | note#label!. Causes that definition to have no backlinks.
166 | note#label^. Causes that definition to have one backlink (to the first ref.)
167 | note#label*. Causes that definition to have all backlinks.
168 |
169 | Any unreferenced notes will be left out of the list unless you explicitly state
170 | you want them by adding a '+'. Like this...
171 |
172 | notelist(class#id)!+. Giving a list of all notes without any backlinks.
173 |
174 | You can mix and match the list backlink control and unreferenced links controls
175 | but the backlink control (if any) must go first. Like so: notelist^+. , not
176 | like this: notelist+^.
177 |
178 | Example...
179 | Scientists say[#lavader] the moon is small.
180 |
181 | note#other. An unreferenced note.
182 |
183 | note#lavader(myliclass). "Proof":http://example.com of a small moon.
184 |
185 | notelist(myclass#myid)+.
186 |
187 | Would output (the actual IDs used would be randomised)...
188 |
189 | Scientists say1 the moon is small.
190 |
191 |
192 | a
193 | Proof of a small moon.
194 | An unreferenced note.
195 |
196 |
197 | The 'a b c' backlink characters can be altered too.
198 | For example if you wanted the notes to have numeric backlinks starting from 1:
199 |
200 | notelist:1.
201 |
202 | Table syntax:
203 |
204 | Simple tables:
205 |
206 | |a|simple|table|row|
207 | |And|Another|table|row|
208 | |With an||empty|cell|
209 |
210 | |=. My table caption goes here
211 | |_. A|_. table|_. header|_.row|
212 | |A|simple|table|row|
213 |
214 | Note: Table captions *must* be the first line of the table else treated as a center-aligned cell.
215 |
216 | Tables with attributes:
217 |
218 | table{border:1px solid black}. My table summary here
219 | {background:#ddd;color:red}. |{}| | | |
220 |
221 | To specify thead / tfoot / tbody groups, add one of these on its own line
222 | above the row(s) you wish to wrap (you may specify attributes before the dot):
223 |
224 | |^. # thead
225 | |-. # tbody
226 | |~. # tfoot
227 |
228 | Column groups:
229 |
230 | |:\3. 100|
231 |
232 | Becomes:
233 |
234 |
235 | You can omit either or both of the \N or width values. You may also
236 | add cells after the colgroup definition to specify col elements with
237 | span, width, or standard Textile attributes:
238 |
239 | |:. 50|(firstcol). |\2. 250||300|
240 |
241 | Becomes:
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 | (Note that, per the HTML specification, you should not add span
250 | to the colgroup if specifying col elements.)
251 |
252 | Applying Attributes:
253 |
254 | Most anywhere Textile code is used, attributes such as arbitrary css style,
255 | css classes, and ids can be applied. The syntax is fairly consistent.
256 |
257 | The following characters quickly alter the alignment of block elements:
258 |
259 | < -> left align ex. p<. left-aligned para
260 | > -> right align h3>. right-aligned header 3
261 | = -> centred h4=. centred header 4
262 | <> -> justified p<>. justified paragraph
263 |
264 | These will change vertical alignment in table cells:
265 |
266 | ^ -> top ex. |^. top-aligned table cell|
267 | - -> middle |-. middle aligned|
268 | ~ -> bottom |~. bottom aligned cell|
269 |
270 | Plain (parentheses) inserted between block syntax and the closing dot-space
271 | indicate classes and ids:
272 |
273 | p(hector). paragraph -> paragraph
274 |
275 | p(#fluid). paragraph -> paragraph
276 |
277 | (classes and ids can be combined)
278 | p(hector#fluid). paragraph -> paragraph
279 |
280 | Curly {brackets} insert arbitrary css style
281 |
282 | p{line-height:18px}. paragraph -> paragraph
283 |
284 | h3{color:red}. header 3 -> header 3
285 |
286 | Square [brackets] insert language attributes
287 |
288 | p[no]. paragraph -> paragraph
289 |
290 | %[fr]phrase% -> phrase
291 |
292 | Usually Textile block element syntax requires a dot and space before the block
293 | begins, but since lists don't, they can be styled just using braces
294 |
295 | #{color:blue} one ->
296 | # big one
297 | # list big
298 | list
299 |
300 |
301 | Using the span tag to style a phrase
302 |
303 | It goes like this, %{color:red}the fourth the fifth%
304 | -> It goes like this, the fourth the fifth
305 |
306 | Ordered list start and continuation:
307 |
308 | You can control the start attribute of an ordered list like so;
309 |
310 | #5 Item 5
311 | # Item 6
312 |
313 | You can resume numbering list items after some intervening anonymous block like so...
314 |
315 | #_ Item 7
316 | # Item 8
317 | */
318 |
319 | namespace Netcarver\Textile;
320 |
321 | /**
322 | * Textile parser.
323 | *
324 | * The Parser class takes Textile input and converts it to well formatted HTML.
325 | * This is the library's main class, hosting the parsing functionality and
326 | * exposing a simple public interface for you to use.
327 | *
328 | * The most basic use case would involve initialising a instance of the class
329 | * and calling the Parser::parse() method:
330 | *
331 | * bc. $parser = new \Netcarver\Textile\Parser();
332 | * echo $parser->parse('h1. Hello World!');
333 | *
334 | * The above generates:
335 | *
336 | * bc. Hello World!
337 | *
338 | * The functionality of the parser can be customized with the setters:
339 | *
340 | * bc. $parser = new \Netcarver\Textile\Parser();
341 | * $parser->setImages(false)->parse('!no-image.jpg!');
342 | *
343 | * The Parser class can also be extended to create pre-configured classes:
344 | *
345 | * bc.. namespace MyApp;
346 | *
347 | * use \Netcarver\Textile\Parser;
348 | *
349 | * class CommentParser extends Parser
350 | * {
351 | * protected function configure()
352 | * {
353 | * $this->setImages(false)->setRestricted(true)->setLite(true);
354 | * }
355 | * }
356 | *
357 | * p. Keep in mind that the classes' protected methods and properties should be
358 | * considered part of the private API and depending on them should be avoided.
359 | * Instead try to only use the public methods marked as being part of the
360 | * public API.
361 | *
362 | * @see Parser::__construct()
363 | * @see Parser::parse()
364 | */
365 |
366 | class Parser
367 | {
368 | /**
369 | * HTML5 document type.
370 | *
371 | * @since 3.8.0
372 | */
373 | const DOCTYPE_HTML5 = 'html5';
374 |
375 | /**
376 | * XHTML document type.
377 | *
378 | * @since 3.8.0
379 | */
380 | const DOCTYPE_XHTML = 'xhtml';
381 |
382 | /**
383 | * Version number.
384 | *
385 | * @var string
386 | */
387 | protected $ver = '4.1.4-dev';
388 |
389 | /**
390 | * Regular expression snippets.
391 | *
392 | * @var string[]
393 | */
394 | protected $regex_snippets = array();
395 |
396 | /**
397 | * Pattern for horizontal align.
398 | *
399 | * @var string
400 | */
401 | protected $hlgn = "(?:\<(?!>)|<>|>|<|(?|\<\>|\=|[()]+(?! ))";
402 |
403 | /**
404 | * Pattern for vertical align.
405 | *
406 | * @var string
407 | */
408 | protected $vlgn = "[\-^~]";
409 |
410 | /**
411 | * Pattern for HTML classes and IDs.
412 | *
413 | * Does not allow classes/ids/languages/styles to span across
414 | * newlines if used in a dotall regular expression.
415 | *
416 | * @var string
417 | */
418 | protected $clas = "(?:\([^)\n]+\))";
419 |
420 | /**
421 | * Pattern for language attribute.
422 | *
423 | * @var string
424 | */
425 | protected $lnge = "(?:\[[^]\n]+\])";
426 |
427 | /**
428 | * Pattern for style attribute.
429 | *
430 | * @var string
431 | */
432 | protected $styl = "(?:\{[^}\n]+\})";
433 |
434 | /**
435 | * Regular expression pattern for column spans in tables.
436 | *
437 | * @var string
438 | */
439 | protected $cspn = "(?:\\\\[0-9]+)";
440 |
441 | /**
442 | * Regular expression for row spans in tables.
443 | *
444 | * @var string
445 | */
446 | protected $rspn = "(?:\/[0-9]+)";
447 |
448 | /**
449 | * Regular expression for horizontal or vertical alignment.
450 | *
451 | * @var string
452 | */
453 | protected $a;
454 |
455 | /**
456 | * Regular expression for column or row spans in tables.
457 | *
458 | * @var string
459 | */
460 | protected $s;
461 |
462 | /**
463 | * Pattern that matches a class, style, language and horizontal alignment attributes.
464 | *
465 | * @var string
466 | */
467 | protected $c;
468 |
469 | /**
470 | * Pattern that matches class, style and language attributes.
471 | *
472 | * Allows all 16 possible permutations of class, style and language attributes.
473 | * No attribute, c, cl, cs, cls, csl, l, lc, ls, lcs, lsc, s, sc, sl, scl or slc.
474 | *
475 | * @var string
476 | */
477 | protected $cls;
478 |
479 | /**
480 | * Whitelisted block tags.
481 | *
482 | * @var string[]
483 | */
484 | protected $blocktag_whitelist = array();
485 |
486 | /**
487 | * Whether raw blocks are enabled.
488 | *
489 | * @var bool
490 | * @since 3.7.0
491 | */
492 | protected $rawBlocksEnabled = false;
493 |
494 | /**
495 | * An array of patterns used for matching phrasing tags.
496 | *
497 | * Phrasing tags, unline others, are wrapped in a paragraph even if they
498 | * already wrap the block.
499 | *
500 | * @var string[]
501 | * @since 3.7.0
502 | */
503 | protected $phrasingContent = array(
504 | 'a',
505 | 'abbr',
506 | 'acronym',
507 | 'area',
508 | 'audio',
509 | 'b',
510 | 'bdo',
511 | 'br',
512 | 'button',
513 | 'canvas',
514 | 'cite',
515 | 'code',
516 | 'command',
517 | 'data',
518 | 'datalist',
519 | 'del',
520 | 'dfn',
521 | 'em',
522 | 'embed',
523 | 'i',
524 | 'iframe',
525 | 'img',
526 | 'input',
527 | 'ins',
528 | 'kbd',
529 | 'keygen',
530 | 'label',
531 | 'link',
532 | 'map',
533 | 'mark',
534 | 'math',
535 | 'meta',
536 | 'meter',
537 | 'noscript',
538 | 'object',
539 | 'output',
540 | 'progress',
541 | 'q',
542 | 'ruby',
543 | 'samp',
544 | 'script',
545 | 'select',
546 | 'small',
547 | 'span',
548 | 'strong',
549 | 'sub',
550 | 'sup',
551 | 'svg',
552 | 'textarea',
553 | 'time',
554 | 'var',
555 | 'video',
556 | 'wbr',
557 | );
558 |
559 | /**
560 | * An array of patterns used to match divider tags.
561 | *
562 | * Blocks containing only self-closing divider tags are not wrapped in
563 | * paragraph tags.
564 | *
565 | * @var string[]
566 | * @since 3.7.0
567 | */
568 | protected $dividerContent = array(
569 | 'br',
570 | 'hr',
571 | 'img',
572 | );
573 |
574 | /**
575 | * An array of patterns used to match unwrappable block tags.
576 | *
577 | * Blocks containing any of these unwrappable tags will not be wrapped in
578 | * paragraphs.
579 | *
580 | * @var string[]
581 | * @since 3.7.0
582 | */
583 | protected $blockContent = array(
584 | 'address',
585 | 'article',
586 | 'aside',
587 | 'blockquote',
588 | 'details',
589 | 'div',
590 | 'dl',
591 | 'fieldset',
592 | 'figure',
593 | 'footer',
594 | 'form',
595 | 'h1',
596 | 'h2',
597 | 'h3',
598 | 'h4',
599 | 'h5',
600 | 'h6',
601 | 'header',
602 | 'hgroup',
603 | 'main',
604 | 'menu',
605 | 'nav',
606 | 'ol',
607 | 'p',
608 | 'pre',
609 | 's',
610 | 'section',
611 | 'table',
612 | 'template',
613 | 'ul',
614 | );
615 |
616 | /**
617 | * An array of built patterns.
618 | *
619 | * @var array|null
620 | * @since 3.7.0
621 | */
622 | protected $patterns = null;
623 |
624 | /**
625 | * Whether block tags are enabled.
626 | *
627 | * @var bool
628 | * @since 3.6.0
629 | */
630 | protected $blockTagsEnabled = true;
631 |
632 | /**
633 | * Whether lines are wrapped.
634 | *
635 | * @var bool
636 | * @since 3.6.0
637 | */
638 | protected $lineWrapEnabled = true;
639 |
640 | /**
641 | * Whether aligning with class selectors is enabled.
642 | *
643 | * @var bool|null
644 | */
645 | protected $isAlignClassesEnabled = null;
646 |
647 | /**
648 | * Pattern for punctation.
649 | *
650 | * @var string
651 | */
652 | protected $pnct = '[\!"#\$%&\'()\*\+,\-\./:;<=>\?@\[\\\]\^_`{\|}\~]';
653 |
654 | /**
655 | * Pattern for URL.
656 | *
657 | * @var string
658 | */
659 | protected $urlch;
660 |
661 | /**
662 | * Matched marker symbols.
663 | *
664 | * @var string
665 | */
666 | protected $syms = '¤§µ¶†‡•∗∴◊♠♣♥♦';
667 |
668 | /**
669 | * HTML rel attribute used for links.
670 | *
671 | * @var string
672 | */
673 | protected $rel = '';
674 |
675 | /**
676 | * Array of footnotes.
677 | *
678 | * @var array
679 | */
680 | protected $fn = array();
681 |
682 | /**
683 | * Shelved content.
684 | *
685 | * Stores fragments of the source text that have been parsed
686 | * and require no more processing.
687 | *
688 | * @var array
689 | */
690 | protected $shelf = array();
691 |
692 | /**
693 | * Restricted mode.
694 | *
695 | * @var bool
696 | */
697 | protected $restricted = false;
698 |
699 | /**
700 | * Disallow images.
701 | *
702 | * @var bool
703 | */
704 | protected $noimage = false;
705 |
706 | /**
707 | * Lite mode.
708 | *
709 | * @var bool
710 | */
711 | protected $lite = false;
712 |
713 | /**
714 | * Accepted link protocols.
715 | *
716 | * @var string[]
717 | */
718 | protected $url_schemes = array();
719 |
720 | /**
721 | * Restricted link protocols.
722 | *
723 | * @var string[]
724 | */
725 | protected $restricted_url_schemes = array(
726 | 'http',
727 | 'https',
728 | 'ftp',
729 | 'mailto',
730 | );
731 |
732 | /**
733 | * Unrestricted link protocols.
734 | *
735 | * @var string[]
736 | */
737 | protected $unrestricted_url_schemes = array(
738 | 'http',
739 | 'https',
740 | 'ftp',
741 | 'mailto',
742 | 'file',
743 | 'tel',
744 | 'callto',
745 | 'sftp',
746 | );
747 |
748 | /**
749 | * Span tags.
750 | *
751 | * @var array
752 | */
753 | protected $span_tags = array(
754 | '*' => 'strong',
755 | '**' => 'b',
756 | '??' => 'cite',
757 | '_' => 'em',
758 | '__' => 'i',
759 | '-' => 'del',
760 | '%' => 'span',
761 | '+' => 'ins',
762 | '~' => 'sub',
763 | '^' => 'sup',
764 | );
765 |
766 | /**
767 | * Span wrappers.
768 | *
769 | * @var array
770 | * @since 3.7.2
771 | */
772 | protected $spanWrappers = array(
773 | '[' => ']',
774 | );
775 |
776 | /**
777 | * Patterns for finding glyphs.
778 | *
779 | * An array of regex patterns used to find text features
780 | * such as apostrophes, fractions and em-dashes. Each
781 | * entry in this array must have a corresponding entry in
782 | * the $glyph_replace array.
783 | *
784 | * @var string[]
785 | * @see Parser::$glyph_replace
786 | */
787 | protected $glyph_search = array();
788 |
789 | /**
790 | * Glyph replacements.
791 | *
792 | * An array of replacements used to insert typographic glyphs
793 | * into the text. Each entry must have a corresponding entry in
794 | * the $glyph_search array and may refer to values captured in
795 | * the corresponding search regex.
796 | *
797 | * @var array
798 | * @see Parser::$glyph_search
799 | */
800 | protected $glyph_replace = array();
801 |
802 | /**
803 | * Indicates whether glyph substitution is required.
804 | *
805 | * Dirty flag, set by Parser::setSymbol(), indicating the parser needs to
806 | * rebuild the glyph substitutions before the next parse.
807 | *
808 | * @var bool
809 | * @see Parser::setSymbol()
810 | */
811 | protected $rebuild_glyphs = true;
812 |
813 | /**
814 | * Relative image path.
815 | *
816 | * @var string
817 | * @deprecated in 3.7.0
818 | * @see Parser::$relImagePrefix
819 | * @see Parser::$relLinkPrefix
820 | */
821 | protected $relativeImagePrefix;
822 |
823 | /**
824 | * Relative link prefix.
825 | *
826 | * @var string
827 | * @since 3.7.0
828 | */
829 | protected $relLinkPrefix = '';
830 |
831 | /**
832 | * Prefix applied to relative images.
833 | *
834 | * @var string
835 | * @since 3.7.0
836 | */
837 | protected $relImagePrefix = '';
838 |
839 | /**
840 | * Maximum nesting level for inline elements.
841 | *
842 | * @var int
843 | */
844 | protected $max_span_depth = 5;
845 |
846 | /**
847 | * Server document root.
848 | *
849 | * @var string
850 | */
851 | protected $doc_root;
852 |
853 | /**
854 | * Target document type.
855 | *
856 | * @var string
857 | */
858 | protected $doctype;
859 |
860 | /**
861 | * An array of supported doctypes.
862 | *
863 | * @var string[]
864 | * @since 3.6.0
865 | */
866 | protected $doctypes = array(
867 | self::DOCTYPE_XHTML,
868 | self::DOCTYPE_HTML5,
869 | );
870 |
871 | /**
872 | * Substitution symbols.
873 | *
874 | * Basic symbols used in textile glyph replacements. To override these, call
875 | * setSymbol method before calling Parser::parse().
876 | *
877 | * @var array
878 | * @see Parser::setSymbol()
879 | * @see Parser::parse()
880 | */
881 | protected $symbols = array(
882 | 'quote_single_open' => '‘',
883 | 'quote_single_close' => '’',
884 | 'quote_double_open' => '“',
885 | 'quote_double_close' => '”',
886 | 'apostrophe' => '’',
887 | 'prime' => '′',
888 | 'prime_double' => '″',
889 | 'ellipsis' => '…',
890 | 'emdash' => '—',
891 | 'endash' => '–',
892 | 'dimension' => '×',
893 | 'trademark' => '™',
894 | 'registered' => '®',
895 | 'copyright' => '©',
896 | 'half' => '½',
897 | 'quarter' => '¼',
898 | 'threequarters' => '¾',
899 | 'degrees' => '°',
900 | 'plusminus' => '±',
901 | 'fn_ref_pattern' => '{marker} ',
902 | 'fn_foot_pattern' => '{marker} ',
903 | 'nl_ref_pattern' => '{marker} ',
904 | 'caps' => '{content} ',
905 | 'acronym' => null,
906 | );
907 |
908 | /**
909 | * Dimensionless images flag.
910 | *
911 | * @var bool
912 | */
913 | protected $dimensionless_images = false;
914 |
915 | /**
916 | * Directory separator.
917 | *
918 | * @var string
919 | */
920 | protected $ds = '/';
921 |
922 | /**
923 | * Whether mbstring extension is installed.
924 | *
925 | * @var bool
926 | */
927 | protected $mb;
928 |
929 | /**
930 | * Multi-byte conversion map.
931 | *
932 | * @var int[]
933 | */
934 | protected $cmap = array(0x0080, 0xffff, 0, 0xffff);
935 |
936 | /**
937 | * Stores note index.
938 | *
939 | * @var int
940 | */
941 | protected $note_index = 1;
942 |
943 | /**
944 | * Stores unreferenced notes.
945 | *
946 | * @var array|int|string>>
947 | */
948 | protected $unreferencedNotes = array();
949 |
950 | /**
951 | * Stores note lists.
952 | *
953 | * @var array
954 | */
955 | protected $notelist_cache = array();
956 |
957 | /**
958 | * Stores notes.
959 | *
960 | * @var array|int|string>>
961 | */
962 | protected $notes = array();
963 |
964 | /**
965 | * Stores URL references.
966 | *
967 | * @var array
968 | */
969 | protected $urlrefs = array();
970 |
971 | /**
972 | * Stores span depth.
973 | *
974 | * @var int
975 | */
976 | protected $span_depth = 0;
977 |
978 | /**
979 | * Unique ID used for reference tokens.
980 | *
981 | * @var string
982 | */
983 | protected $uid;
984 |
985 | /**
986 | * Token reference index.
987 | *
988 | * @var int
989 | */
990 | protected $refIndex = 1;
991 |
992 | /**
993 | * Stores references values.
994 | *
995 | * @var array
996 | */
997 | protected $refCache = array();
998 |
999 | /**
1000 | * Matched open and closed quotes.
1001 | *
1002 | * @var array
1003 | */
1004 | protected $quotes = array(
1005 | '"' => '"',
1006 | "'" => "'",
1007 | '(' => ')',
1008 | '{' => '}',
1009 | '[' => ']',
1010 | '«' => '»',
1011 | '»' => '«',
1012 | '‹' => '›',
1013 | '›' => '‹',
1014 | '„' => '“',
1015 | '‚' => '‘',
1016 | '‘' => '’',
1017 | '”' => '“',
1018 | );
1019 |
1020 | /**
1021 | * Regular expression that matches starting quotes.
1022 | *
1023 | * @var string
1024 | */
1025 | protected $quote_starts;
1026 |
1027 | /**
1028 | * Ordered list starts.
1029 | *
1030 | * @var array
1031 | */
1032 | protected $olstarts = array();
1033 |
1034 | /**
1035 | * Link prefix.
1036 | *
1037 | * @var string
1038 | */
1039 | protected $linkPrefix;
1040 |
1041 | /**
1042 | * Link index.
1043 | *
1044 | * @var int
1045 | */
1046 | protected $linkIndex = 1;
1047 |
1048 | /**
1049 | * Constructor.
1050 | *
1051 | * The constructor allows setting options that affect the class instance as
1052 | * a whole, such as the output doctype. To instruct the parser to return
1053 | * HTML5 markup instead of XHTML, set $doctype argument to 'html5'.
1054 | *
1055 | * bc. use Netcarver\Textile\Parser;
1056 | * $parser = new Parser(Parser::DOCTYPE_HTML5);
1057 | * echo $parser->parse('HTML(HyperText Markup Language)");
1058 | *
1059 | * @param string $doctype The output document type, either 'xhtml' or 'html5'
1060 | * @throws \InvalidArgumentException
1061 | * @see Parser::configure()
1062 | * @see Parser::parse()
1063 | * @see Parser::setDocumentType()
1064 | * @see Parser::DOCTYPE_HTML5
1065 | * @see Parser::DOCTYPE_XHTML
1066 | * @api
1067 | */
1068 | public function __construct($doctype = 'xhtml')
1069 | {
1070 | $this->setDocumentType($doctype)->setRestricted(false);
1071 | $uid = uniqid((string) rand());
1072 | $this->uid = 'textileRef:'.$uid.':';
1073 | $this->linkPrefix = $uid.'-';
1074 | $this->a = "(?:$this->hlgn|$this->vlgn)*";
1075 | $this->s = "(?:$this->cspn|$this->rspn)*";
1076 | $this->c = "(?:$this->clas|$this->styl|$this->lnge|$this->hlgn)*";
1077 |
1078 | $this->cls = '(?:'.
1079 | "$this->clas(?:".
1080 | "$this->lnge(?:$this->styl)?|$this->styl(?:$this->lnge)?".
1081 | ')?|'.
1082 | "$this->lnge(?:".
1083 | "$this->clas(?:$this->styl)?|$this->styl(?:$this->clas)?".
1084 | ')?|'.
1085 | "$this->styl(?:".
1086 | "$this->clas(?:$this->lnge)?|$this->lnge(?:$this->clas)?".
1087 | ')?'.
1088 | ')?';
1089 |
1090 | if ($this->isUnicodePcreSupported()) {
1091 | $this->regex_snippets = array(
1092 | 'acr' => '\p{Lu}\p{Nd}',
1093 | 'abr' => '\p{Lu}',
1094 | 'nab' => '\p{Ll}',
1095 | 'wrd' => '(?:\p{L}|\p{M}|\p{N}|\p{Pc})',
1096 | 'mod' => 'u', // Make sure to mark the unicode patterns as such, Some servers seem to need this.
1097 | 'cur' => '\p{Sc}',
1098 | 'digit' => '\p{N}',
1099 | 'space' => '(?:\p{Zs}|\h|\v)',
1100 | 'char' => '(?:[^\p{Zs}\h\v])',
1101 | );
1102 | } else {
1103 | $this->regex_snippets = array(
1104 | 'acr' => 'A-Z0-9',
1105 | 'abr' => 'A-Z',
1106 | 'nab' => 'a-z',
1107 | 'wrd' => '\w',
1108 | 'mod' => '',
1109 | 'cur' => '',
1110 | 'digit' => '\d',
1111 | 'space' => '(?:\s|\h|\v)',
1112 | 'char' => '\S',
1113 | );
1114 | }
1115 |
1116 | $this->urlch = '['.$this->regex_snippets['wrd'].'"$\-_.+!*\'(),";\/?:@=&%#{}|\\^~\[\]`]';
1117 | $this->quote_starts = implode('|', array_map('preg_quote', array_keys($this->quotes)));
1118 |
1119 | if (defined('DIRECTORY_SEPARATOR')) {
1120 | $this->ds = DIRECTORY_SEPARATOR;
1121 | }
1122 |
1123 | if (php_sapi_name() === 'cli') {
1124 | if (($cwd = getcwd()) !== false) {
1125 | $this->setDocumentRootDirectory($cwd);
1126 | }
1127 | } elseif (!empty($_SERVER['DOCUMENT_ROOT'])) {
1128 | $this->setDocumentRootDirectory($_SERVER['DOCUMENT_ROOT']);
1129 | } elseif (!empty($_SERVER['PATH_TRANSLATED'])) {
1130 | $this->setDocumentRootDirectory($_SERVER['PATH_TRANSLATED']);
1131 | }
1132 |
1133 | $this->configure();
1134 | }
1135 |
1136 | /**
1137 | * Configure the current parser.
1138 | *
1139 | * This method can be extended to create a pre-configured parser class.
1140 | *
1141 | * bc.. namespace MyApp;
1142 | *
1143 | * use Netcarver\Textile\Parser;
1144 | *
1145 | * class CommentParser extends Parser
1146 | * {
1147 | * protected function configure()
1148 | * {
1149 | * $this->setImages(false)->setRestricted(true)->setLite(true);
1150 | * }
1151 | * }
1152 | *
1153 | * @since 3.7.0
1154 | * @return void Return value is ignored
1155 | * @api
1156 | */
1157 | protected function configure()
1158 | {
1159 | }
1160 |
1161 | /**
1162 | * Sets the output document type.
1163 | *
1164 | * bc. use Netcarver\Textile\Parser;
1165 | * $parser = new Parser();
1166 | * echo $parser
1167 | * ->setDocumentType(Parser::DOCTYPE_HTML5)
1168 | * ->parse('HTML(HyperText Markup Language)");
1169 | *
1170 | * @param string $doctype Either 'xhtml' or 'html5'
1171 | * @return Parser This instance
1172 | * @since 3.6.0
1173 | * @see Parser::getDocumentType()
1174 | * @see Parser::DOCTYPE_HTML5
1175 | * @see Parser::DOCTYPE_XHTML
1176 | * @api
1177 | */
1178 | public function setDocumentType($doctype)
1179 | {
1180 | if (in_array($doctype, $this->doctypes, true)) {
1181 | if ($this->getDocumentType() !== $doctype) {
1182 | $this->doctype = $doctype;
1183 | $this->rebuild_glyphs = true;
1184 | }
1185 |
1186 | return $this;
1187 | }
1188 |
1189 | throw new \InvalidArgumentException('Invalid doctype given.');
1190 | }
1191 |
1192 | /**
1193 | * Gets the current output document type.
1194 | *
1195 | * bc. $parser = new \Netcarver\Textile\Parser();
1196 | * echo $parser->getDocumentType();
1197 | *
1198 | * @return string The document type
1199 | * @since 3.6.0
1200 | * @see Parser::setDocumentType()
1201 | * @api
1202 | */
1203 | public function getDocumentType()
1204 | {
1205 | return $this->doctype;
1206 | }
1207 |
1208 | /**
1209 | * Sets the document root directory path.
1210 | *
1211 | * This method sets the path that is used to resolve relative file paths
1212 | * within local filesystem. This is used to fetch image dimensions, for
1213 | * instance.
1214 | *
1215 | * bc. $parser = new \Netcarver\Textile\Parser();
1216 | * $parser->setDocumentRootDirectory('/path/to/document/root/dir');
1217 | *
1218 | * If not set, document root defaults to the current working directory if
1219 | * PHP-Textile is used via CLI. On server environment, DOCUMENT_ROOT or
1220 | * PATH_TRANSLATED server variable is used based on which ever is available.
1221 | *
1222 | * @param string $path The root path
1223 | * @return Parser This instance
1224 | * @since 3.6.0
1225 | * @see Parser::getDocumentRootDirectory()
1226 | * @api
1227 | */
1228 | public function setDocumentRootDirectory($path)
1229 | {
1230 | $this->doc_root = rtrim($path, '\\/').$this->ds;
1231 | return $this;
1232 | }
1233 |
1234 | /**
1235 | * Gets the current document root directory path.
1236 | *
1237 | * bc. $parser = new \Netcarver\Textile\Parser();
1238 | * echo $parser->getDocumentRootDirectory();
1239 | *
1240 | * @return string Path to the document root directory
1241 | * @since 3.6.0
1242 | * @see Parser::setDocumentRootDirectory()
1243 | * @api
1244 | */
1245 | public function getDocumentRootDirectory()
1246 | {
1247 | return $this->doc_root;
1248 | }
1249 |
1250 | /**
1251 | * Enables lite mode.
1252 | *
1253 | * If enabled, allowed tags are limited. Parser will prevent the use extra
1254 | * Textile formatting, accepting only paragraphs and blockquotes as valid
1255 | * block tags.
1256 | *
1257 | * bc. $parser = new \Netcarver\Textile\Parser();
1258 | * $parser
1259 | * ->setLite(true)
1260 | * ->parse('h1. Headings are disabled too');
1261 | *
1262 | * Generates:
1263 | *
1264 | * bc. h1. Headings are disabled too
1265 | *
1266 | * This doesn't prevent unsafe input values. If you wish to parse untrusted
1267 | * user-given Textile input, also enable the restricted parser mode with
1268 | * Parser::setRestricted().
1269 | *
1270 | * bc. $parser = new \Netcarver\Textile\Parser();
1271 | * echo $parser
1272 | * ->setRestricted(true)
1273 | * ->setLite(true)
1274 | * ->parse('h1. Hello World!');
1275 | *
1276 | * @param bool $lite TRUE to enable
1277 | * @return Parser This instance
1278 | * @since 3.6.0
1279 | * @see Parser::isLiteModeEnabled()
1280 | * @see Parser::setRestricted()
1281 | * @api
1282 | */
1283 | public function setLite($lite)
1284 | {
1285 | $this->lite = (bool) $lite;
1286 | return $this;
1287 | }
1288 |
1289 | /**
1290 | * Gets the lite mode status.
1291 | *
1292 | * bc. $parser = new \Netcarver\Textile\Parser();
1293 | * if ($parser->isLiteModeEnabled() === true) {
1294 | * echo 'Lite mode is enabled.';
1295 | * }
1296 | *
1297 | * @return bool TRUE if enabled, FALSE otherwise
1298 | * @since 3.6.0
1299 | * @see Parser::setLite()
1300 | * @api
1301 | */
1302 | public function isLiteModeEnabled()
1303 | {
1304 | return (bool) $this->lite;
1305 | }
1306 |
1307 | /**
1308 | * Disables and enables images.
1309 | *
1310 | * If disabled, image tags are not generated. This option is ideal for
1311 | * minimalist output such as text-only comments.
1312 | *
1313 | * bc. $parser = new \Netcarver\Textile\Parser();
1314 | * echo $parser
1315 | * ->setImages(true)
1316 | * ->parse('!image.png!');
1317 | *
1318 | * Generates:
1319 | *
1320 | * bc. !image.png!
1321 | *
1322 | * @param bool $enabled TRUE to enable, FALSE to disable
1323 | * @return Parser This instance
1324 | * @since 3.6.0
1325 | * @see Parser::isImageTagEnabled()
1326 | * @api
1327 | */
1328 | public function setImages($enabled)
1329 | {
1330 | $this->noimage = !$enabled;
1331 | return $this;
1332 | }
1333 |
1334 | /**
1335 | * Whether images are enabled.
1336 | *
1337 | * bc. $parser = new \Netcarver\Textile\Parser();
1338 | * if ($parser->isImageTagEnabled() === true) {
1339 | * echo 'Images are enabled.';
1340 | * }
1341 | *
1342 | * @return bool TRUE if enabled, FALSE otherwise
1343 | * @since 3.6.0
1344 | * @see Parser::setImages()
1345 | * @api
1346 | */
1347 | public function isImageTagEnabled()
1348 | {
1349 | return !$this->noimage;
1350 | }
1351 |
1352 | /**
1353 | * Sets link relationship status value.
1354 | *
1355 | * This method sets the HTML relationship tokens that are applied to links
1356 | * generated by PHP-Textile.
1357 | *
1358 | * bc. $parser = new \Netcarver\Textile\Parser();
1359 | * echo $parser
1360 | * ->setLinkRelationShip('nofollow')
1361 | * ->parse('"Link":http://example.com/');
1362 | *
1363 | * Generates:
1364 | *
1365 | * bc. Link
1366 | *
1367 | * @param string|array $relationship The HTML rel attribute value
1368 | * @return Parser This instance
1369 | * @since 3.6.0
1370 | * @see Parser::getLinkRelationShip()
1371 | * @api
1372 | */
1373 | public function setLinkRelationShip($relationship)
1374 | {
1375 | $this->rel = (string) implode(' ', (array) $relationship);
1376 | return $this;
1377 | }
1378 |
1379 | /**
1380 | * Gets the link relationship status value.
1381 | *
1382 | * bc. $parser = new \Netcarver\Textile\Parser();
1383 | * echo $parse
1384 | * ->setLinkRelationShip('nofollow')
1385 | * ->getLinkRelationShip();
1386 | *
1387 | * The above outputs "nofollow".
1388 | *
1389 | * @return string The value
1390 | * @since 3.6.0
1391 | * @see Parser::setLinkRelationShip()
1392 | * @api
1393 | */
1394 | public function getLinkRelationShip()
1395 | {
1396 | return $this->rel;
1397 | }
1398 |
1399 | /**
1400 | * Enables restricted parser mode.
1401 | *
1402 | * This option should be enabled when parsing untrusted user input,
1403 | * including comments or forum posts. When enabled, the parser escapes any
1404 | * raw HTML input, ignores unsafe attributes and links only whitelisted URL
1405 | * schemes.
1406 | *
1407 | * For instance the following malicious input:
1408 | *
1409 | * bc. $parser = new \Netcarver\Textile\Parser();
1410 | * echo $parser
1411 | * ->setRestricted(true)
1412 | * ->parse('Innocent _looking_ "link":javacript:window.alert().');
1413 | *
1414 | * Returns safe, sanitized HTML with valid Textile input still parsed:
1415 | *
1416 | * bc. Innocent looking “link”:javacript:window.alert().
1417 | *
1418 | * If left disabled, the parser allows users to mix raw HTML and Textile.
1419 | * Using the parser in non-restricted on untrusted input, like comments
1420 | * and forum posts, will lead to XSS issues, as users will be able to use
1421 | * any HTML code, JavaScript links and Textile attributes in their input.
1422 | *
1423 | * @param bool $enabled TRUE to enable, FALSE to disable
1424 | * @return Parser This instance
1425 | * @since 3.6.0
1426 | * @see Parser::isRestrictedModeEnabled()
1427 | * @api
1428 | */
1429 | public function setRestricted($enabled)
1430 | {
1431 | if ($enabled) {
1432 | $this->url_schemes = $this->restricted_url_schemes;
1433 | $this->restricted = true;
1434 | } else {
1435 | $this->url_schemes = $this->unrestricted_url_schemes;
1436 | $this->restricted = false;
1437 | }
1438 |
1439 | return $this;
1440 | }
1441 |
1442 | /**
1443 | * Whether restricted parser mode is enabled.
1444 | *
1445 | * bc. $parser = new \Netcarver\Textile\Parser();
1446 | * if ($parser->isRestrictedModeEnabled() === true) {
1447 | * echo 'PHP-Textile is in restricted mode.';
1448 | * }
1449 | *
1450 | * @return bool TRUE if enabled, FALSE otherwise
1451 | * @since 3.6.0
1452 | * @see Parser::setRestricted()
1453 | * @api
1454 | */
1455 | public function isRestrictedModeEnabled()
1456 | {
1457 | return (bool) $this->restricted;
1458 | }
1459 |
1460 | /**
1461 | * Enables and disables raw blocks.
1462 | *
1463 | * When raw blocks are enabled, any paragraph blocks wrapped in a tag
1464 | * not matching Parser::$blockContent or Parser::$phrasingContent will not
1465 | * be parsed, and instead is left as is.
1466 | *
1467 | * bc. $parser = new \Netcarver\Textile\Parser();
1468 | * echo $parser
1469 | * ->setRawBlocks(true)
1470 | * ->parse('A *raw* block.
');
1471 | *
1472 | * The above generates:
1473 | *
1474 | * bc. A *raw* block.
1475 | *
1476 | * @param bool $enabled TRUE to enable, FALSE to disable
1477 | * @return Parser This instance
1478 | * @since 3.7.0
1479 | * @see Parser::isRawBlocksEnabled()
1480 | * @see Parser::isRawBlock()
1481 | * @api
1482 | */
1483 | public function setRawBlocks($enabled)
1484 | {
1485 | $this->rawBlocksEnabled = (bool) $enabled;
1486 | return $this;
1487 | }
1488 |
1489 | /**
1490 | * Whether raw blocks are enabled.
1491 | *
1492 | * bc. $parser = new \Netcarver\Textile\Parser();
1493 | * if ($parser->isRawBlocksEnabled() === true) {
1494 | * echo 'Raw blocks are enabled';
1495 | * }
1496 | *
1497 | * @return bool TRUE if enabled, FALSE otherwise
1498 | * @since 3.7.0
1499 | * @see Parser::setRawBlocks()
1500 | * @api
1501 | */
1502 | public function isRawBlocksEnabled()
1503 | {
1504 | return (bool) $this->rawBlocksEnabled;
1505 | }
1506 |
1507 | /**
1508 | * Sets class alignment mode independent of the document type.
1509 | *
1510 | * In HTML5 document type, img elements are generated with align-left,
1511 | * align-center and align-right class selectors rather than align
1512 | * attribute being added to the image.
1513 | *
1514 | * With this option you can enable that functionality in XHTML document type mode too.
1515 | *
1516 | * bc. $parser = new \Netcarver\Textile\Parser();
1517 | * $parser
1518 | * ->setAlignClasses(true)
1519 | * ->parse(!
1524 | *
1525 | * @param bool $enabled TRUE to enable, FALSE to disable
1526 | * @return Parser This instance
1527 | * @since 3.8.0
1528 | * @api
1529 | */
1530 | public function setAlignClasses($enabled)
1531 | {
1532 | $this->isAlignClassesEnabled = (bool) $enabled;
1533 | return $this;
1534 | }
1535 |
1536 | /**
1537 | * Whether class alignment mode is enabled.
1538 | *
1539 | * bc. $parser = new \Netcarver\Textile\Parser();
1540 | * if ($parser->isAlignClassesEnabled() === true) {
1541 | * echo 'Images are aligned with class instead of align attribute';
1542 | * }
1543 | *
1544 | * @return bool TRUE if enabled, FALSE otherwise
1545 | * @since 3.8.0
1546 | * @see Parser::setAlignClasses()
1547 | * @api
1548 | */
1549 | public function isAlignClassesEnabled()
1550 | {
1551 | if ($this->isAlignClassesEnabled === null
1552 | && $this->getDocumentType() === self::DOCTYPE_HTML5
1553 | ) {
1554 | return true;
1555 | }
1556 |
1557 | return (bool) $this->isAlignClassesEnabled;
1558 | }
1559 |
1560 | /**
1561 | * Enables and disables block-level tags and formatting features.
1562 | *
1563 | * When disabled, block-level tags aren't rendered. This allows PHP-Textile
1564 | * to operate on a single line of text, rather than blocks of text and does
1565 | * not wrap the output in paragraph tags.
1566 | *
1567 | * bc. $parser = new \Netcarving\Textile\Parser();
1568 | * echo $parser
1569 | * ->setBlockTags(false)
1570 | * ->parse('h1. Hello *strong* world!');
1571 | *
1572 | * The above generates:
1573 | *
1574 | * bc. h1. Hello strong world!
1575 | *
1576 | * @param bool $enabled TRUE to enable, FALSE to disable
1577 | * @return Parser This instance
1578 | * @since 3.6.0
1579 | * @see Parser::isBlockTagEnabled()
1580 | * @api
1581 | */
1582 | public function setBlockTags($enabled)
1583 | {
1584 | $this->blockTagsEnabled = (bool) $enabled;
1585 | return $this;
1586 | }
1587 |
1588 | /**
1589 | * Whether block-level tags are enabled and parsed.
1590 | *
1591 | * bc. $parser = new \Netcarving\Textile\Parser();
1592 | * if ($parser->isBlockTagAllowed() === true) {
1593 | * echo 'Block tags are enabled.';
1594 | * }
1595 | *
1596 | * @return bool TRUE if enabled, FALSE otherwise
1597 | * @since 3.6.0
1598 | * @see Parser::setBlockTags()
1599 | * @api
1600 | */
1601 | public function isBlockTagEnabled()
1602 | {
1603 | return (bool) $this->blockTagsEnabled;
1604 | }
1605 |
1606 | /**
1607 | * Enables and disables line-wrapping.
1608 | *
1609 | * If enabled, line-breaks are replaced by target document's break tag. If
1610 | * disabled, input document's line-breaks are ignored. This setting can be
1611 | * used if the the input document's lines are pre-wrapped. For instance,
1612 | * in case the input is from CLI content, or source code documentation.
1613 | *
1614 | * bc. $parser = new \Netcarving\Textile\Parser();
1615 | * echo $parser
1616 | * ->setLineWrap(false)
1617 | * ->parse("Hello\nworld!");
1618 | *
1619 | * The above generates:
1620 | *
1621 | * bc. Hello world!
1622 | *
1623 | * @param bool $enabled TRUE to enable, FALSE to disable
1624 | * @return Parser This instance
1625 | * @since 3.6.0
1626 | * @see Parser::isLineWrapEnabled()
1627 | * @api
1628 | */
1629 | public function setLineWrap($enabled)
1630 | {
1631 | $this->lineWrapEnabled = (bool) $enabled;
1632 | return $this;
1633 | }
1634 |
1635 | /**
1636 | * Whether line-wrapping is enabled.
1637 | *
1638 | * bc. $parser = new \Netcarving\Textile\Parser();
1639 | * if ($parser->isLineWrapEnabled() === true) {
1640 | * echo 'Line-wrapping is enabled.';
1641 | * }
1642 | *
1643 | * @return bool TRUE if enabled, FALSE otherwise
1644 | * @see Parser::setLineWrap()
1645 | * @since 3.6.0
1646 | * @api
1647 | */
1648 | public function isLineWrapEnabled()
1649 | {
1650 | return (bool) $this->lineWrapEnabled;
1651 | }
1652 |
1653 | /**
1654 | * Sets a substitution symbol.
1655 | *
1656 | * This method lets you to redefine a substitution symbol. The following
1657 | * sets the 'half' glyph:
1658 | *
1659 | * bc. $parser = new \Netcarver\Textile\Parser();
1660 | * echo $parser
1661 | * ->setSymbol('half', '1⁄2')
1662 | * ->parse('Hello [1/2] World!');
1663 | *
1664 | * Generates:
1665 | *
1666 | * bc. Hello 1⁄2 World!
1667 | *
1668 | * Symbol can be set to FALSE to disable it:
1669 | *
1670 | * bc. $parser = new \Netcarver\Textile\Parser();
1671 | * $parser->setSymbol('dimension', false);
1672 | *
1673 | * See Parser::getSymbol() to find out all available symbols.
1674 | *
1675 | * @param string $name Name of the symbol to assign a new value to
1676 | * @param string|bool $value New value for the symbol, or FALSE to disable
1677 | * @return Parser This instance
1678 | * @see Parser::getSymbol()
1679 | * @api
1680 | */
1681 | public function setSymbol($name, $value)
1682 | {
1683 | if ($value !== false) {
1684 | $value = (string) $value;
1685 | }
1686 |
1687 | $this->symbols[(string) $name] = $value;
1688 | $this->rebuild_glyphs = true;
1689 | return $this;
1690 | }
1691 |
1692 | /**
1693 | * Gets a symbol definitions.
1694 | *
1695 | * This method gets a symbol definition by name, or the full symbol table
1696 | * as an array.
1697 | *
1698 | * bc. $parser = new \Netcarver\Textile\Parser();
1699 | * echo $parser->getSymbol('dimension');
1700 | *
1701 | * To get all available symbol definitions:
1702 | *
1703 | * bc. $parser = new \Netcarver\Textile\Parser();
1704 | * print_r($parser->getSymbol());
1705 | *
1706 | * @param string|null $name The name of the symbol, or NULL if requesting the symbol table
1707 | * @return array|string|bool The symbol table or the requested symbol
1708 | * @throws \InvalidArgumentException
1709 | * @see Parser::setSymbol()
1710 | * @api
1711 | */
1712 | public function getSymbol($name = null)
1713 | {
1714 | if ($name !== null) {
1715 | if (isset($this->symbols[$name])) {
1716 | return $this->symbols[$name];
1717 | }
1718 |
1719 | throw new \InvalidArgumentException('The specified name does not match any symbols.');
1720 | }
1721 |
1722 | return $this->symbols;
1723 | }
1724 |
1725 | /**
1726 | * Sets base relative image prefix.
1727 | *
1728 | * The given string is used to prefix relative image paths, usually an
1729 | * absolute HTTP address pointing a the site's image, or upload, directory.
1730 | * PHP-Textile to convert relative paths to absolute, or prefixed paths.
1731 | *
1732 | * bc. $parser = new \Netcarver\Textile\Parser();
1733 | * $parser->setImagePrefix('https://static.example.com/images/');
1734 | *
1735 | * @param string $prefix The prefix
1736 | * @return Parser This instance
1737 | * @since 3.7.0
1738 | * @see Parser::getImagePrefix()
1739 | * @api
1740 | */
1741 | public function setImagePrefix($prefix)
1742 | {
1743 | $this->relImagePrefix = (string) $prefix;
1744 | return $this;
1745 | }
1746 |
1747 | /**
1748 | * Gets base relative image prefix.
1749 | *
1750 | * bc. $parser = new \Netcarver\Textile\Parser();
1751 | * echo $parser->getImagePrefix();
1752 | *
1753 | * @return string The prefix
1754 | * @since 3.7.0
1755 | * @see Parser::setImagePrefix()
1756 | * @api
1757 | */
1758 | public function getImagePrefix()
1759 | {
1760 | return (string) $this->relImagePrefix;
1761 | }
1762 |
1763 | /**
1764 | * Sets base relative link prefix.
1765 | *
1766 | * The given string is used to prefix relative link paths. This allows
1767 | * PHP-Textile convert relative paths to absolute, or prefixed, links.
1768 | *
1769 | * bc. $parser = new \Netcarver\Textile\Parser();
1770 | * $parser->setLinkPrefix('https://example.com/');
1771 | *
1772 | * @param string $prefix The prefix
1773 | * @return Parser This instance
1774 | * @since 3.7.0
1775 | * @see Parser::getLinkPrefix()
1776 | * @api
1777 | */
1778 | public function setLinkPrefix($prefix)
1779 | {
1780 | $this->relLinkPrefix = (string) $prefix;
1781 | return $this;
1782 | }
1783 |
1784 | /**
1785 | * Gets base relative link prefix.
1786 | *
1787 | * bc. $parser = new \Netcarver\Textile\Parser();
1788 | * echo $parser->getLinkPrefix();
1789 | *
1790 | * @return string The prefix
1791 | * @since 3.7.0
1792 | * @see Parser::setLinkPrefix()
1793 | * @api
1794 | */
1795 | public function getLinkPrefix()
1796 | {
1797 | return (string) $this->relLinkPrefix;
1798 | }
1799 |
1800 | /**
1801 | * Sets base relative image and link directory path.
1802 | *
1803 | * This is used when Textile is supplied with a relative image or link path.
1804 | * Allows client systems to have PHP-Textile convert relative paths to
1805 | * absolute or prefixed paths. This method is used to set that base path,
1806 | * usually an absolute HTTP address pointing to a directory. Note that
1807 | * despite its name it applies to both links and images.
1808 | *
1809 | * bc. $parser = new \Netcarver\Textile\Parser();
1810 | * $parser->setRelativeImagePrefix('https://example.com/');
1811 | *
1812 | * @param string $prefix The string to prefix all relative image paths with
1813 | * @return Parser This instance
1814 | * @deprecated in 3.7.0
1815 | * @see Parser::setImagePrefix
1816 | * @see Parser::setLinkPrefix
1817 | * @api
1818 | */
1819 | public function setRelativeImagePrefix($prefix = '')
1820 | {
1821 | trigger_error(
1822 | 'Parser::setRelativeImagePrefix() is deprecated.'.
1823 | 'Use Parser::setImagePrefix() and Parser::setLinkPrefix() instead.',
1824 | E_USER_DEPRECATED
1825 | );
1826 |
1827 | $this->relativeImagePrefix = $prefix;
1828 | return $this;
1829 | }
1830 |
1831 | /**
1832 | * Enables dimensionless images.
1833 | *
1834 | * If enabled, image width and height attributes will not be included in
1835 | * rendered image tags. Normally, PHP-Textile will add width and height
1836 | * to images linked with a local relative path, as long as the image file
1837 | * can be accessed.
1838 | *
1839 | * bc. $parser = new \Netcarver\Textile\Parser();
1840 | * echo $parser
1841 | * ->setDimensionlessImages(true)
1842 | * ->parse('!image.jpg!');
1843 | *
1844 | * @param bool $dimensionless TRUE to disable image dimensions, FALSE to enable
1845 | * @return Parser This instance
1846 | * @see Parser::getDimensionlessImages()
1847 | * @api
1848 | */
1849 | public function setDimensionlessImages($dimensionless = true)
1850 | {
1851 | $this->dimensionless_images = (bool) $dimensionless;
1852 | return $this;
1853 | }
1854 |
1855 | /**
1856 | * Whether dimensionless images are enabled.
1857 | *
1858 | * bc. $parser = new \Netcarver\Textile\Parser();
1859 | * if ($parser->getDimensionlessImages() === true) {
1860 | * echo 'Images do not get dimensions.';
1861 | * }
1862 | *
1863 | * @return bool TRUE if images will not get dimensions, FALSE otherwise
1864 | * @see Parser::setDimensionlessImages()
1865 | * @api
1866 | */
1867 | public function getDimensionlessImages()
1868 | {
1869 | return (bool) $this->dimensionless_images;
1870 | }
1871 |
1872 | /**
1873 | * Gets PHP-Textile version number.
1874 | *
1875 | * bc. $parser = new \Netcarver\Textile\Parser();
1876 | * echo $parser->getVersion();
1877 | *
1878 | * @return string Version number
1879 | * @api
1880 | */
1881 | public function getVersion()
1882 | {
1883 | return $this->ver;
1884 | }
1885 |
1886 | /**
1887 | * Encodes the given text.
1888 | *
1889 | * bc. $parser = new \Netcarver\Textile\Parser();
1890 | * $parser->textileEncode('Some content to encode.');
1891 | *
1892 | * @param string $text The text to be encoded
1893 | * @return string The encoded text
1894 | * @api
1895 | */
1896 | public function textileEncode($text)
1897 | {
1898 | return (string)preg_replace('/&(?!(?:[a-z][a-z\d]*|#(?:\d+|x[a-f\d]+));)/i', '&', $text);
1899 | }
1900 |
1901 | /**
1902 | * Parses the given Textile input according to the previously set options.
1903 | *
1904 | * The parser's features can be changed by using the various public setter
1905 | * methods this class has. The most basic use case is:
1906 | *
1907 | * bc. $parser = new \Netcarver\Textile\Parser();
1908 | * echo $parser->parse('h1. Hello World!');
1909 | *
1910 | * The above parses trusted input in full-feature mode, generating:
1911 | *
1912 | * bc. Hello World!
1913 | *
1914 | * Additionally the parser can be run in safe, restricted mode using the
1915 | * Parser::setRestricted() method.
1916 | *
1917 | * bc. $parser = new \Netcarver\Textile\Parser();
1918 | * echo $parser
1919 | * ->setRestricted(true)
1920 | * ->parse('h1. Hello World!');
1921 | *
1922 | * This enables restricted mode and allows safe parsing of untrusted input.
1923 | * PHP-Textile will disable unsafe attributes, links and escapes any raw
1924 | * HTML input. This option should be enabled when parsing untrusted user
1925 | * input.
1926 | *
1927 | * If restricted mode is disabled, the parser allows users to mix raw HTML
1928 | * and Textile.
1929 | *
1930 | * @param string $text The Textile input to parse
1931 | * @return string Parsed Textile input
1932 | * @since 3.6.0
1933 | * @api
1934 | */
1935 | public function parse($text)
1936 | {
1937 | $this->prepare();
1938 | $text = (string) $text;
1939 |
1940 | if ($this->isRestrictedModeEnabled()) {
1941 | // Escape any raw HTML.
1942 | $text = $this->encodeHTML($text, false);
1943 | }
1944 |
1945 | $text = $this->cleanWhiteSpace($text);
1946 | $text = $this->cleanUniqueTokens($text);
1947 |
1948 | if ($this->isBlockTagEnabled()) {
1949 | if ($this->isLiteModeEnabled()) {
1950 | $this->blocktag_whitelist = array('bq', 'p');
1951 | $text = $this->blocks($text."\n\n");
1952 | } else {
1953 | $this->blocktag_whitelist = array(
1954 | 'bq',
1955 | 'p',
1956 | 'bc',
1957 | 'notextile',
1958 | 'pre',
1959 | 'h[1-6]',
1960 | 'fn'.$this->regex_snippets['digit'].'+',
1961 | '###',
1962 | );
1963 | $text = $this->blocks($text);
1964 | $text = $this->placeNoteLists($text);
1965 | }
1966 | } else {
1967 | $text .= "\n\n";
1968 |
1969 | // Treat quoted quote as a special glyph.
1970 | $text = $this->glyphQuotedQuote($text);
1971 |
1972 | // Inline markup (em, strong, sup, sub, del etc).
1973 | $text = $this->spans($text);
1974 |
1975 | // Generate links.
1976 | $text = $this->links($text);
1977 |
1978 | // Glyph level substitutions (mainly typographic -- " & ' => curly quotes, -- => em-dash etc.
1979 | $text = $this->glyphs($text);
1980 | }
1981 |
1982 | $text = $this->retrieve($text);
1983 | $text = $this->replaceGlyphs($text);
1984 | $text = $this->retrieveTags($text);
1985 | $text = $this->retrieveURLs($text);
1986 |
1987 | // Replace shelved instances that were inside tag and link attributes.
1988 | $text = $this->retrieve($text);
1989 |
1990 | $text = str_replace($this->getLineBreak(), $this->getLineBreak()."\n", $text);
1991 |
1992 | return $text;
1993 | }
1994 |
1995 | /**
1996 | * Parses the given Textile input in un-restricted mode.
1997 | *
1998 | * This method is deprecated, use Parser::parse() method instead.
1999 | * This method is equivalent of:
2000 | *
2001 | * bc. $parser = new \Netcarver\Textile\Parser();
2002 | * echo $parser->parse('h1. Hello World!');
2003 | *
2004 | * Additional arguments can be passed with setter methods:
2005 | *
2006 | * bc. $parser = new \Netcarver\Textile\Parser();
2007 | * echo $parser
2008 | * ->setLite(true)
2009 | * ->setImages(true)
2010 | * ->setLinkRelationShip('nofollow')
2011 | * ->parse('h1. Hello World!');
2012 | *
2013 | * @param string $text The Textile input to parse
2014 | * @param bool $lite Switch to lite mode
2015 | * @param bool $encode Encode input and return
2016 | * @param bool $noimage Disables images
2017 | * @param bool $strict This argument is ignored
2018 | * @param string $rel Relationship attribute applied to generated links
2019 | * @return string Parsed $text
2020 | * @see Parser::parse()
2021 | * @deprecated in 3.6.0
2022 | * @api
2023 | */
2024 | public function textileThis($text, $lite = false, $encode = false, $noimage = false, $strict = false, $rel = '')
2025 | {
2026 | if ($encode) {
2027 | trigger_error(
2028 | '$encode argument is deprecated. Use Parser::textileEncode() instead.',
2029 | \E_USER_DEPRECATED
2030 | );
2031 |
2032 | return $this->textileEncode($text);
2033 | }
2034 |
2035 | trigger_error(
2036 | 'Parser::textileThis() is deprecated. Use Parser::parse() instead.',
2037 | \E_USER_DEPRECATED
2038 | );
2039 |
2040 | return $this
2041 | ->setRestricted(false)
2042 | ->setLite($lite)
2043 | ->setBlockTags(true)
2044 | ->setImages(!$noimage)
2045 | ->setLinkRelationShip($rel)
2046 | ->parse($text);
2047 | }
2048 |
2049 | /**
2050 | * Parses the given Textile input in restricted mode.
2051 | *
2052 | * This method is deprecated, use Parser::parse() method with
2053 | * Parser::setRestricted() and Parser::setLite() enabled, and
2054 | * Parser::setImages() disabled.
2055 | *
2056 | * This method's defaults are identical to:
2057 | *
2058 | * bc. $parser = new \Netcarver\Textile\Parser();
2059 | * echo $parser
2060 | * ->setRestricted(true)
2061 | * ->setLite(true)
2062 | * ->setImages(false)
2063 | * ->setLinkRelationShip('nofollow')
2064 | * ->parse('h1. Hello World!');
2065 | *
2066 | * As in the above, restricted mode should be used when parsing any
2067 | * untrusted user input, including comments or forum posts.
2068 | *
2069 | * @param string $text The Textile input to parse
2070 | * @param bool $lite Controls lite mode, allowing extra formatting
2071 | * @param bool $noimage Allow images
2072 | * @param string $rel Relationship attribute applied to generated links
2073 | * @return string Parsed input
2074 | * @see Parser::setRestricted()
2075 | * @see Parser::setLite()
2076 | * @see Parser::setImages()
2077 | * @see Parser::setLinkRelationShip()
2078 | * @see Parser::parse()
2079 | * @deprecated in 3.6.0
2080 | * @api
2081 | */
2082 | public function textileRestricted($text, $lite = true, $noimage = true, $rel = 'nofollow')
2083 | {
2084 | trigger_error(
2085 | 'Parser::textileRestricted() is deprecated. Use Parser::parse() with Parser::setRestricted() instead.',
2086 | E_USER_DEPRECATED
2087 | );
2088 |
2089 | return $this
2090 | ->setRestricted(true)
2091 | ->setLite($lite)
2092 | ->setBlockTags(true)
2093 | ->setImages(!$noimage)
2094 | ->setLinkRelationShip($rel)
2095 | ->parse($text);
2096 | }
2097 |
2098 | /**
2099 | * Parses Textile syntax.
2100 | *
2101 | * This method performs common parse actions.
2102 | *
2103 | * @param string $text The input to parse
2104 | * @param bool $lite Enables lite mode
2105 | * @return string Parsed input
2106 | * @deprecated in 3.6.0
2107 | */
2108 | protected function textileCommon($text, $lite)
2109 | {
2110 | trigger_error('Parser::textileCommon() is deprecated.', E_USER_DEPRECATED);
2111 | return $this->setLite($lite)->parse($text);
2112 | }
2113 |
2114 | /**
2115 | * Output line break according to document type.
2116 | *
2117 | * @return string The break tag
2118 | * @since 4.0.0
2119 | * @see Parser::getDocumentType()
2120 | */
2121 | protected function getLineBreak()
2122 | {
2123 | return ($this->getDocumentType() === self::DOCTYPE_HTML5) ? ' ' : ' ';
2124 | }
2125 |
2126 | /**
2127 | * Prepares the glyph patterns from the symbol table.
2128 | *
2129 | * @return void
2130 | * @see Parser::setSymbol()
2131 | * @see Parser::getSymbol()
2132 | */
2133 | protected function prepGlyphs()
2134 | {
2135 | if ($this->rebuild_glyphs === false) {
2136 | return;
2137 | }
2138 |
2139 | $pnc = '[[:punct:]]';
2140 | $cur = '';
2141 |
2142 | if ($this->regex_snippets['cur']) {
2143 | $cur = '(?:['.$this->regex_snippets['cur'].']'.$this->regex_snippets['space'].'*)?';
2144 | }
2145 |
2146 | $this->glyph_search = array();
2147 | $this->glyph_replace = array();
2148 |
2149 | // Dimension sign
2150 | if ($this->symbols['dimension'] !== false) {
2151 | $this->glyph_search[] = '/(?<=\b|x)([0-9]++[\])]?[\'"]? ?)[x]( ?[\[(]?)(?=[+-]?'.$cur.'[0-9]*\.?[0-9]++)/i'.
2152 | $this->regex_snippets['mod'];
2153 | $this->glyph_replace[] = '$1'.$this->symbols['dimension'].'$2';
2154 | }
2155 |
2156 | // Apostrophe
2157 | if ($this->symbols['apostrophe'] !== false) {
2158 | $this->glyph_search[] = '/('.$this->regex_snippets['wrd'].'|\))\''.
2159 | '('.$this->regex_snippets['wrd'].')/'.$this->regex_snippets['mod'];
2160 | $this->glyph_replace[] = '$1'.$this->symbols['apostrophe'].'$2';
2161 |
2162 | // Back in '88/the '90s but not in his '90s', '1', '1.' '10m' or '5.png'
2163 | $this->glyph_search[] = '/('.$this->regex_snippets['space'].')\''.
2164 | '(\d+'.$this->regex_snippets['wrd'].'?)\b(?![.]?['.$this->regex_snippets['wrd'].']*?\')/'.
2165 | $this->regex_snippets['mod'];
2166 | $this->glyph_replace[] = '$1'.$this->symbols['apostrophe'].'$2';
2167 | }
2168 |
2169 | // Single open following open bracket
2170 | if ($this->symbols['quote_single_open'] !== false) {
2171 | $this->glyph_search[] = "/([([{])'(?=\S)/".$this->regex_snippets['mod'];
2172 | $this->glyph_replace[] = '$1'.$this->symbols['quote_single_open'];
2173 | }
2174 |
2175 | // Single closing
2176 | if ($this->symbols['quote_single_close'] !== false) {
2177 | $this->glyph_search[] = '/(\S)\'(?='.$this->regex_snippets['space'].'|'.$pnc.'|<|$)/'.
2178 | $this->regex_snippets['mod'];
2179 | $this->glyph_replace[] = '$1'.$this->symbols['quote_single_close'];
2180 | }
2181 |
2182 | // Default single opening
2183 | if ($this->symbols['quote_single_open'] !== false) {
2184 | $this->glyph_search[] = "/'/";
2185 | $this->glyph_replace[] = $this->symbols['quote_single_open'];
2186 | }
2187 |
2188 | // Double open following an open bracket. Allows things like Hello ["(Mum) & dad"]
2189 | if ($this->symbols['quote_double_open'] !== false) {
2190 | $this->glyph_search[] = '/([([{])"(?=\S)/'.$this->regex_snippets['mod'];
2191 | $this->glyph_replace[] = '$1'.$this->symbols['quote_double_open'];
2192 | }
2193 |
2194 | // Double closing
2195 | if ($this->symbols['quote_double_close'] !== false) {
2196 | $this->glyph_search[] = '/(\S)"(?='.$this->regex_snippets['space'].'|'.$pnc.'|<|$)/'.
2197 | $this->regex_snippets['mod'];
2198 | $this->glyph_replace[] = '$1'.$this->symbols['quote_double_close'];
2199 | }
2200 |
2201 | // Default double opening
2202 | if ($this->symbols['quote_double_open'] !== false) {
2203 | $this->glyph_search[] = '/"/';
2204 | $this->glyph_replace[] = $this->symbols['quote_double_open'];
2205 | }
2206 |
2207 | if ($this->symbols['acronym'] === null) {
2208 | if ($this->getDocumentType() === 'html5') {
2209 | $acronym = '{content} ';
2210 | } else {
2211 | $acronym = '{content} ';
2212 | }
2213 | } else {
2214 | $acronym = $this->symbols['acronym'];
2215 | }
2216 |
2217 | // 3+ uppercase acronym
2218 | if ($acronym !== false) {
2219 | $this->glyph_search[] = '/\b(['.$this->regex_snippets['abr'].']['.
2220 | $this->regex_snippets['acr'].']{2,})\b(?:[(]([^)]*)[)])/'.$this->regex_snippets['mod'];
2221 | $this->glyph_replace[] = $this->replaceMarkers((string) $acronym, array(
2222 | 'title' => '$2',
2223 | 'content' => '$1',
2224 | ));
2225 | }
2226 |
2227 | // 3+ uppercase
2228 | if ($this->symbols['caps'] !== false) {
2229 | $this->glyph_search[] = '/('.$this->regex_snippets['space'].'|^|[>(;-])'.
2230 | '(['.$this->regex_snippets['abr'].']{3,})'.
2231 | '(['.$this->regex_snippets['nab'].']*)(?='.
2232 | $this->regex_snippets['space'].'|'.$pnc.'|<|$)'.
2233 | '(?=[^">]*?(<|$))/'.$this->regex_snippets['mod'];
2234 | $this->glyph_replace[] = $this->replaceMarkers('$1'.$this->symbols['caps'].'$3', array(
2235 | 'content' => $this->uid.':glyph:$2',
2236 | ));
2237 | }
2238 |
2239 | // Ellipsis
2240 | if ($this->symbols['ellipsis'] !== false) {
2241 | $this->glyph_search[] = '/([^.]?)\.{3}/';
2242 | $this->glyph_replace[] = '$1'.$this->symbols['ellipsis'];
2243 | }
2244 |
2245 | // em dash
2246 | if ($this->symbols['emdash'] !== false) {
2247 | $this->glyph_search[] = '/--/';
2248 | $this->glyph_replace[] = $this->symbols['emdash'];
2249 | }
2250 |
2251 | // en dash
2252 | if ($this->symbols['endash'] !== false) {
2253 | $this->glyph_search[] = '/ - /';
2254 | $this->glyph_replace[] = ' '.$this->symbols['endash'].' ';
2255 | }
2256 |
2257 | // Trademark
2258 | if ($this->symbols['trademark'] !== false) {
2259 | $this->glyph_search[] = '/(\b ?|'.$this->regex_snippets['space'].'|^)[([]TM[])]/i'.
2260 | $this->regex_snippets['mod'];
2261 | $this->glyph_replace[] = '$1'.$this->symbols['trademark'];
2262 | }
2263 |
2264 | // Registered
2265 | if ($this->symbols['registered'] !== false) {
2266 | $this->glyph_search[] = '/(\b ?|'.$this->regex_snippets['space'].'|^)[([]R[])]/i'.
2267 | $this->regex_snippets['mod'];
2268 | $this->glyph_replace[] = '$1'.$this->symbols['registered'];
2269 | }
2270 |
2271 | // Copyright
2272 | if ($this->symbols['copyright'] !== false) {
2273 | $this->glyph_search[] = '/(\b ?|'.$this->regex_snippets['space'].'|^)[([]C[])]/i'.
2274 | $this->regex_snippets['mod'];
2275 | $this->glyph_replace[] = '$1'.$this->symbols['copyright'];
2276 | }
2277 |
2278 | // 1/4
2279 | if ($this->symbols['quarter'] !== false) {
2280 | $this->glyph_search[] = '/[([]1\/4[])]/';
2281 | $this->glyph_replace[] = $this->symbols['quarter'];
2282 | }
2283 |
2284 | // 1/2
2285 | if ($this->symbols['half'] !== false) {
2286 | $this->glyph_search[] = '/[([]1\/2[])]/';
2287 | $this->glyph_replace[] = $this->symbols['half'];
2288 | }
2289 |
2290 | // 3/4
2291 | if ($this->symbols['threequarters'] !== false) {
2292 | $this->glyph_search[] = '/[([]3\/4[])]/';
2293 | $this->glyph_replace[] = $this->symbols['threequarters'];
2294 | }
2295 |
2296 | // Degrees -- that's a small 'oh'
2297 | if ($this->symbols['degrees'] !== false) {
2298 | $this->glyph_search[] = '/[([]o[])]/';
2299 | $this->glyph_replace[] = $this->symbols['degrees'];
2300 | }
2301 |
2302 | // Plus minus
2303 | if ($this->symbols['plusminus'] !== false) {
2304 | $this->glyph_search[] = '/[([]\+\/-[])]/';
2305 | $this->glyph_replace[] = $this->symbols['plusminus'];
2306 | }
2307 |
2308 | // No need to rebuild next run unless a symbol is redefined
2309 | $this->rebuild_glyphs = false;
2310 | }
2311 |
2312 | /**
2313 | * Gets the maximum allowed link index.
2314 | *
2315 | * @return int Maximum link index
2316 | * @since 3.5.5
2317 | */
2318 | protected function getMaxLinkIndex()
2319 | {
2320 | return 1000000;
2321 | }
2322 |
2323 | /**
2324 | * Prepares the parser for parsing.
2325 | *
2326 | * This method prepares the transient internal state of
2327 | * Textile parser in preparation for parsing a new document.
2328 | *
2329 | * @param bool|null $lite Controls lite mode
2330 | * @param bool|null $noimage Disallow images
2331 | * @param string|null $rel A relationship attribute applied to links
2332 | * @return void
2333 | */
2334 | protected function prepare($lite = null, $noimage = null, $rel = null)
2335 | {
2336 | if ($this->linkIndex >= $this->getMaxLinkIndex()) {
2337 | $this->linkPrefix .= '-';
2338 | $this->linkIndex = 1;
2339 | }
2340 |
2341 | $this->unreferencedNotes = array();
2342 | $this->notelist_cache = array();
2343 | $this->notes = array();
2344 | $this->urlrefs = array();
2345 | $this->shelf = array();
2346 | $this->fn = array();
2347 | $this->span_depth = 0;
2348 | $this->refIndex = 1;
2349 | $this->refCache = array();
2350 | $this->note_index = 1;
2351 |
2352 | if ($lite !== null) {
2353 | trigger_error(
2354 | '$lite argument is deprecated. Use Parser::setLite() instead.',
2355 | E_USER_DEPRECATED
2356 | );
2357 |
2358 | $this->setLite($lite);
2359 | }
2360 |
2361 | if ($noimage !== null) {
2362 | trigger_error(
2363 | '$noimage argument is deprecated. Use Parser::setImages() instead.',
2364 | E_USER_DEPRECATED
2365 | );
2366 |
2367 | $this->setImages(!$noimage);
2368 | }
2369 |
2370 | if ($rel !== null) {
2371 | trigger_error(
2372 | '$rel argument is deprecated. Use Parser::setRelative() instead.',
2373 | E_USER_DEPRECATED
2374 | );
2375 |
2376 | $this->setLinkRelationShip($rel);
2377 | }
2378 |
2379 | if ($this->patterns === null) {
2380 | $block = implode('|', $this->blockContent);
2381 | $divider = implode('|', $this->dividerContent);
2382 | $phrasing = implode('|', $this->phrasingContent);
2383 |
2384 | $this->patterns = array(
2385 | 'block' => '/^(?:'.$block.')$/i',
2386 | 'contained' => '/^<\/?(?P[^\s<>\/]+)(?:\s.*|\/?>.*|)>$/si',
2387 | 'divider' => '/^(?:<\/?('.$divider.')(?:\s[^<>]*?|\/?)>(?:<\/\1\s*?>)?)+$/si',
2388 | 'phrasing' => '/^(?:'.$phrasing.')$/i',
2389 | 'wrapped' => '/^<\/?(?P[^\s<>\/]+)[^<>]*?>(?:.*<\/\1\s*?>)?$/si',
2390 | 'unwrappable' => '/<\/?(?:'.$block.')(?:\s[^<>]*?|\/?)>/si',
2391 | );
2392 | }
2393 |
2394 | $this->prepGlyphs();
2395 | }
2396 |
2397 | /**
2398 | * Cleans a HTML attribute value.
2399 | *
2400 | * This method checks for presence of URL encoding in the value.
2401 | * If the number encoded characters exceeds the threshold,
2402 | * the input is discarded. Otherwise the encoded
2403 | * instances are decoded.
2404 | *
2405 | * This method also strips any ", ' and = characters
2406 | * from the given value. This method does not guarantee
2407 | * valid HTML or full sanitization.
2408 | *
2409 | * @param string $in The input string
2410 | * @return string Cleaned string
2411 | */
2412 | protected function cleanAttribs($in)
2413 | {
2414 | $tmp = $in;
2415 | $before = -1;
2416 | $after = 0;
2417 | $max = 3;
2418 | $i = 0;
2419 |
2420 | while (($after != $before) && ($i < $max)) {
2421 | $before = strlen($tmp);
2422 | $tmp = rawurldecode($tmp);
2423 | $after = strlen($tmp);
2424 | $i++;
2425 | }
2426 |
2427 | if ($i === $max) {
2428 | // If we hit the max allowed decodes, assume the input is tainted and consume it.
2429 | $out = '';
2430 | } else {
2431 | $out = str_replace(array('"', "'", '='), '', $tmp);
2432 | }
2433 |
2434 | return $out;
2435 | }
2436 |
2437 | /**
2438 | * Constructs a HTML tag from an object.
2439 | *
2440 | * This is a helper method that creates a new
2441 | * instance of \Netcarver\Textile\Tag.
2442 | *
2443 | * @param string $name The HTML element name
2444 | * @param array $atts HTML attributes applied to the tag
2445 | * @param bool $selfclosing Determines if the tag should be self-closing
2446 | * @return Tag
2447 | */
2448 | protected function newTag($name, $atts, $selfclosing = true)
2449 | {
2450 | return new Tag($name, $atts, $selfclosing && $this->getDocumentType() !== self::DOCTYPE_HTML5);
2451 | }
2452 |
2453 | /**
2454 | * Parses Textile attributes.
2455 | *
2456 | * @param string $in The Textile attribute string to be parsed
2457 | * @param string $element Focus the routine to interpret the attributes as applying to a specific HTML tag
2458 | * @param bool $include_id If FALSE, IDs are not included in the attribute list
2459 | * @param string $autoclass An additional classes applied to the output
2460 | * @return string HTML attribute list
2461 | * @see Parser::parseAttribsToArray()
2462 | */
2463 | protected function parseAttribs($in, $element = '', $include_id = true, $autoclass = '')
2464 | {
2465 | $o = $this->parseAttribsToArray($in, $element, $include_id, $autoclass);
2466 |
2467 | return $this->formatAttributeString($o);
2468 | }
2469 |
2470 | /**
2471 | * Converts an array of named attribute => value mappings to a string.
2472 | *
2473 | * @param array $attribute_array
2474 | * @return string
2475 | */
2476 | protected function formatAttributeString(array $attribute_array)
2477 | {
2478 | $out = '';
2479 |
2480 | if (count($attribute_array)) {
2481 | foreach ($attribute_array as $k => $v) {
2482 | $out .= " $k=\"$v\"";
2483 | }
2484 | }
2485 |
2486 | return $out;
2487 | }
2488 |
2489 | /**
2490 | * Parses Textile attributes into an array.
2491 | *
2492 | * @param string $in The Textile attribute string to be parsed
2493 | * @param string $element Focus the routine to interpret the attributes as applying to a specific HTML tag
2494 | * @param bool $include_id If FALSE, IDs are not included in the attribute list
2495 | * @param string $autoclass An additional classes applied to the output
2496 | * @return array HTML attributes as key => value mappings
2497 | * @see Parser::parseAttribs()
2498 | */
2499 | protected function parseAttribsToArray($in, $element = '', $include_id = true, $autoclass = '')
2500 | {
2501 | $style = array();
2502 | $class = '';
2503 | $lang = '';
2504 | $colspan = '';
2505 | $rowspan = '';
2506 | $span = '';
2507 | $width = '';
2508 | $id = '';
2509 | $matched = $in;
2510 |
2511 | if ($element == 'td') {
2512 | if (preg_match("/\\\\([0-9]+)/", $matched, $csp)) {
2513 | $colspan = $csp[1];
2514 | }
2515 |
2516 | if (preg_match("/\/([0-9]+)/", $matched, $rsp)) {
2517 | $rowspan = $rsp[1];
2518 | }
2519 | }
2520 |
2521 | if (($element === 'td' || $element === 'tr')
2522 | && preg_match("/^($this->vlgn)/", $matched, $vert)
2523 | ) {
2524 | $style[] = "vertical-align:" . $this->vAlign($vert[1]);
2525 | }
2526 |
2527 | if (preg_match("/\{([^}]*)\}/", $matched, $sty)) {
2528 | if ($sty[1] = $this->cleanAttribs($sty[1])) {
2529 | $style[] = rtrim($sty[1], ';');
2530 | }
2531 |
2532 | $matched = str_replace($sty[0], '', $matched);
2533 | }
2534 |
2535 | if (preg_match("/\(([^()]+)\)/U", $matched, $cls)) {
2536 | $class_regex = "/^([-a-zA-Z 0-9_\.\/\[\]:!]*)$/";
2537 |
2538 | // Consume entire class block -- valid or invalid.
2539 | $matched = str_replace($cls[0], '', $matched);
2540 |
2541 | // Only allow a restricted subset of the CSS standard characters for classes/ids.
2542 | // No encoding markers allowed.
2543 | if (preg_match("/\(([-a-zA-Z 0-9_\/\[\].:!#]+)\)/U", $cls[0], $cls)) {
2544 | $hashpos = strpos($cls[1], '#');
2545 | // If a textile class block attribute was found with a '#' in it
2546 | // split it into the css class and css id...
2547 | if (false !== $hashpos) {
2548 | if (preg_match("/#([-a-zA-Z0-9_\.\:]*)$/", substr($cls[1], $hashpos), $ids)) {
2549 | $id = $ids[1];
2550 | }
2551 |
2552 | if (preg_match($class_regex, substr($cls[1], 0, $hashpos), $ids)) {
2553 | $class = $ids[1];
2554 | }
2555 | } else {
2556 | if (preg_match($class_regex, $cls[1], $ids)) {
2557 | $class = $ids[1];
2558 | }
2559 | }
2560 | }
2561 | }
2562 |
2563 | if (preg_match("/\[([^]]+)\]/U", $matched, $lng)) {
2564 | // Consume entire lang block -- valid or invalid.
2565 | $matched = str_replace($lng[0], '', $matched);
2566 | if ($element === 'code' && preg_match("/\[([a-zA-Z0-9_-]+)\]/U", $lng[0], $lng1)) {
2567 | $lang = $lng1[1];
2568 | } elseif (preg_match("/\[([a-zA-Z]{2}(?:[\-\_][a-zA-Z]{2})?)\]/U", $lng[0], $lng2)) {
2569 | $lang = $lng2[1];
2570 | }
2571 | }
2572 |
2573 | if (preg_match("/(\(+)/", $matched, $pl)) {
2574 | $style[] = "padding-left:" . strlen($pl[1]) . "em";
2575 | $matched = str_replace($pl[0], '', $matched);
2576 | }
2577 |
2578 | if (preg_match("/(\)+)/", $matched, $pr)) {
2579 | $style[] = "padding-right:" . strlen($pr[1]) . "em";
2580 | $matched = str_replace($pr[0], '', $matched);
2581 | }
2582 |
2583 | if (preg_match("/($this->hlgn)/", $matched, $horiz)) {
2584 | $style[] = "text-align:" . $this->hAlign($horiz[1]);
2585 | }
2586 |
2587 | if ($element == 'col'
2588 | && preg_match("/(?:\\\\([0-9]+))?{$this->regex_snippets['space']}*([0-9]+)?/", $matched, $csp)
2589 | ) {
2590 | $span = isset($csp[1]) ? $csp[1] : '';
2591 | $width = isset($csp[2]) ? $csp[2] : '';
2592 | }
2593 |
2594 | if ($this->isRestrictedModeEnabled()) {
2595 | $o = array();
2596 | $class = trim($autoclass);
2597 |
2598 | if ($class) {
2599 | $o['class'] = $this->cleanAttribs($class);
2600 | }
2601 |
2602 | if ($lang) {
2603 | $o['lang'] = $this->cleanAttribs($lang);
2604 | }
2605 |
2606 | ksort($o);
2607 | return $o;
2608 | } else {
2609 | $class = trim($class . ' ' . $autoclass);
2610 | }
2611 |
2612 | $o = array();
2613 |
2614 | if ($class) {
2615 | $o['class'] = $this->cleanAttribs($class);
2616 | }
2617 |
2618 | if ($colspan) {
2619 | $o['colspan'] = $this->cleanAttribs($colspan);
2620 | }
2621 |
2622 | if ($id && $include_id) {
2623 | $o['id'] = $this->cleanAttribs($id);
2624 | }
2625 |
2626 | if ($lang) {
2627 | $o['lang'] = $this->cleanAttribs($lang);
2628 | }
2629 |
2630 | if ($rowspan) {
2631 | $o['rowspan'] = $this->cleanAttribs($rowspan);
2632 | }
2633 |
2634 | if ($span) {
2635 | $o['span'] = $this->cleanAttribs($span);
2636 | }
2637 |
2638 | if (!empty($style)) {
2639 | $so = '';
2640 | $tmps = array();
2641 |
2642 | foreach ($style as $s) {
2643 | $parts = explode(';', $s);
2644 |
2645 | foreach ($parts as $p) {
2646 | if ($p = trim(trim($p), ":")) {
2647 | $tmps[] = $p;
2648 | }
2649 | }
2650 | }
2651 |
2652 | sort($tmps);
2653 |
2654 | foreach ($tmps as $p) {
2655 | // @phpstan-ignore-next-line
2656 | if ($p) {
2657 | $so .= $p.';';
2658 | }
2659 | }
2660 |
2661 | $o['style'] = trim(str_replace(array("\n", ';;'), array('', ';'), $so));
2662 | }
2663 |
2664 | if ($width) {
2665 | $o['width'] = $this->cleanAttribs($width);
2666 | }
2667 |
2668 | ksort($o);
2669 | return $o;
2670 | }
2671 |
2672 | /**
2673 | * Checks whether the text block should be wrapped in a paragraph.
2674 | *
2675 | * @param string $text The input string
2676 | * @return bool TRUE if the text can be wrapped, FALSE otherwise
2677 | */
2678 | protected function hasRawText($text)
2679 | {
2680 | if (preg_match($this->patterns['unwrappable'], $text)) {
2681 | return false;
2682 | }
2683 |
2684 | if (preg_match($this->patterns['divider'], $text)) {
2685 | return false;
2686 | }
2687 |
2688 | if (preg_match($this->patterns['wrapped'], $text, $m)) {
2689 | if (preg_match($this->patterns['phrasing'], $m['open'])) {
2690 | return true;
2691 | }
2692 |
2693 | return false;
2694 | }
2695 |
2696 | return true;
2697 | }
2698 |
2699 | /**
2700 | * Parses textile table structures into HTML.
2701 | *
2702 | * @param string $text The textile input
2703 | * @return string The parsed text
2704 | */
2705 | protected function tables($text)
2706 | {
2707 | $text = $text . "\n\n";
2708 | return (string)preg_replace_callback(
2709 | "/^(?:table(?P_?{$this->s}{$this->a}{$this->cls})\.".
2710 | "(?P.*)?\n)?^(?P{$this->a}{$this->cls}\.? ?\|.*\|){$this->regex_snippets['space']}*\n\n/smU",
2711 | array($this, "fTable"),
2712 | $text
2713 | );
2714 | }
2715 |
2716 | /**
2717 | * Constructs an HTML table from a textile table structure.
2718 | *
2719 | * This method is used by Parser::tables() to process
2720 | * found table structures.
2721 | *
2722 | * @param array $matches
2723 | * @return string HTML table
2724 | * @see Parser::tables()
2725 | */
2726 | protected function fTable($matches)
2727 | {
2728 | $tatts = $this->parseAttribs($matches['tatts'], 'table');
2729 | $space = $this->regex_snippets['space'];
2730 |
2731 | $cap = '';
2732 | $colgrp = '';
2733 | $last_rgrp = '';
2734 | $c_row = 1;
2735 | $sum = '';
2736 | $rows = array();
2737 |
2738 | $summary = trim($matches['summary']);
2739 |
2740 | if ($summary !== '') {
2741 | $sum = ' summary="'.htmlspecialchars($summary, ENT_QUOTES, 'UTF-8').'"';
2742 | }
2743 |
2744 | /** @var array $components */
2745 | $components = preg_split("/\|{$space}*?$/m", $matches['rows'], -1, PREG_SPLIT_NO_EMPTY);
2746 |
2747 | foreach ($components as $row) {
2748 | $row = ltrim($row);
2749 |
2750 | // Caption -- can only occur on row 1, otherwise treat '|=. foo |...'
2751 | // as a normal center-aligned cell.
2752 | if (($c_row <= 1) && preg_match(
2753 | "/^\|\=(?P$this->s$this->a$this->cls)\. (?P[^\n]*)(?P.*)/s",
2754 | ltrim($row),
2755 | $cmtch
2756 | )) {
2757 | $capts = $this->parseAttribs($cmtch['capts']);
2758 | $cap = "\t".trim($cmtch['cap'])." \n";
2759 | $row = ltrim($cmtch['row']);
2760 | if (!$row) {
2761 | continue;
2762 | }
2763 | }
2764 |
2765 | $c_row += 1;
2766 |
2767 | // Colgroup
2768 | if (preg_match("/^\|:(?P$this->s$this->a$this->cls\. .*)/m", ltrim($row), $gmtch)) {
2769 | // Is this colgroup def missing a closing pipe? If so, there
2770 | // will be a newline in the middle of $row somewhere.
2771 | $nl = strpos($row, "\n");
2772 | $idx = 0;
2773 |
2774 | foreach (explode('|', str_replace('.', '', $gmtch['cols'])) as $col) {
2775 | $gatts = $this->parseAttribs(trim($col), 'col');
2776 | $colgrp .= "\t " : $gatts." />")."\n";
2777 | $idx++;
2778 | }
2779 |
2780 | $colgrp .= "\t\n";
2781 |
2782 | if ($nl === false) {
2783 | continue;
2784 | } else {
2785 | // Recover from our missing pipe and process the rest of the line.
2786 | $row = ltrim(substr($row, $nl));
2787 | }
2788 | }
2789 |
2790 | // Row group
2791 | $rgrpatts = $rgrp = '';
2792 |
2793 | if (preg_match(
2794 | "/(:?^\|(?P$this->vlgn)(?P$this->s$this->a$this->cls)\.{$space}*$\n)?^(?P.*)/sm",
2795 | ltrim($row),
2796 | $grpmatch
2797 | )) {
2798 | if (isset($grpmatch['part'])) {
2799 | if ($grpmatch['part'] === '^') {
2800 | $rgrp = 'head';
2801 | } elseif ($grpmatch['part'] === '~') {
2802 | $rgrp = 'foot';
2803 | } elseif ($grpmatch['part'] === '-') {
2804 | $rgrp = 'body';
2805 | }
2806 | }
2807 |
2808 | if (isset($grpmatch['part'])) {
2809 | $rgrpatts = $this->parseAttribs($grpmatch['rgrpatts']);
2810 | }
2811 |
2812 | if (isset($grpmatch['row'])) {
2813 | $row = $grpmatch['row'];
2814 | }
2815 | }
2816 |
2817 | if (preg_match("/^(?P$this->a$this->cls\. )(?P.*)/m", ltrim($row), $rmtch)) {
2818 | $ratts = $this->parseAttribs($rmtch['ratts'], 'tr');
2819 | $row = $rmtch['row'];
2820 | } else {
2821 | $ratts = '';
2822 | }
2823 |
2824 | $cells = array();
2825 | $cellctr = 0;
2826 |
2827 | foreach (explode("|", $row) as $cell) {
2828 | $ctyp = "d";
2829 |
2830 | if (preg_match("/^_(?=[{$this->regex_snippets['space']}[:punct:]])/", $cell)) {
2831 | $ctyp = "h";
2832 | }
2833 |
2834 | if (preg_match("/^(?P_?$this->s$this->a$this->cls\. )(?P.*)/s", $cell, $cmtch)) {
2835 | $catts = $this->parseAttribs($cmtch['catts'], 'td');
2836 | $cell = $cmtch['cell'];
2837 | } else {
2838 | $catts = '';
2839 | }
2840 |
2841 | if (!$this->isLiteModeEnabled()) {
2842 | $a = array();
2843 |
2844 | if (preg_match('/(?'.$this->regex_snippets['space'].'*)(?P.*)/s', $cell, $a)) {
2845 | $cell = $this->redclothLists($a['cell']);
2846 | $cell = $this->textileLists($cell);
2847 | $cell = $a['space'] . $cell;
2848 | }
2849 | }
2850 |
2851 | if ($cellctr > 0) {
2852 | // Ignore first 'cell': it precedes the opening pipe
2853 | $cells[] = $this->doTagBr("t$ctyp", "\t\t\t$cell ");
2854 | }
2855 |
2856 | $cellctr++;
2857 | }
2858 |
2859 | $grp = '';
2860 |
2861 | if ($rgrp && $last_rgrp) {
2862 | $grp .= "\t\n";
2863 | }
2864 |
2865 | if ($rgrp) {
2866 | $grp .= "\t\n";
2867 | }
2868 |
2869 | $last_rgrp = ($rgrp) ? $rgrp : $last_rgrp;
2870 | $rows[] = $grp."\t\t\n" . join("\n", $cells) . ($cells ? "\n" : "") . "\t\t ";
2871 | unset($cells, $catts);
2872 | }
2873 |
2874 | $rows = join("\n", $rows) . "\n";
2875 | $close = '';
2876 |
2877 | if ($last_rgrp) {
2878 | $close = "\t \n";
2879 | }
2880 |
2881 | return "\n".$cap.$colgrp.$rows.$close." \n\n";
2882 | }
2883 |
2884 | /**
2885 | * Parses RedCloth-style definition lists into HTML.
2886 | *
2887 | * @param string $text The textile input
2888 | * @return string The parsed text
2889 | */
2890 | protected function redclothLists($text)
2891 | {
2892 | return (string)preg_replace_callback(
2893 | "/^([-]+$this->cls[ .].*:=.*)$(?![^-])/smU",
2894 | array($this, "fRedclothList"),
2895 | $text
2896 | );
2897 | }
2898 |
2899 | /**
2900 | * Constructs a HTML definition list from a RedCloth-style definition structure.
2901 | *
2902 | * This method is used by Parser::redclothLists() to process
2903 | * found definition list structures.
2904 | *
2905 | * @param array $m
2906 | * @return string HTML definition list
2907 | * @see Parser::redclothLists()
2908 | */
2909 | protected function fRedclothList($m)
2910 | {
2911 | $in = $m[0];
2912 | $out = array();
2913 |
2914 | /** @var array $text */
2915 | $text = preg_split('/\n(?=-)/m', $in);
2916 |
2917 | foreach ($text as $line) {
2918 | $m = array();
2919 |
2920 | if (preg_match("/^[-]+(?P$this->cls)\.? (?P.*)$/s", $line, $m)) {
2921 | $content = trim($m['content']);
2922 | $atts = $this->parseAttribs($m['atts']);
2923 |
2924 | if (!preg_match(
2925 | "/^(.*?){$this->regex_snippets['space']}*:=(.*?)".
2926 | "{$this->regex_snippets['space']}*(=:|:=)?".
2927 | "{$this->regex_snippets['space']}*$/s",
2928 | $content,
2929 | $xm
2930 | )) {
2931 | $xm = array( $content, $content, '' );
2932 | }
2933 |
2934 | list(, $term, $def,) = $xm;
2935 | $term = trim($term);
2936 | $def = trim($def, ' ');
2937 |
2938 | if (!$out) {
2939 | if ($def === '') {
2940 | $out[] = "";
2941 | } else {
2942 | $out[] = '';
2943 | }
2944 | }
2945 |
2946 | if ($term !== '') {
2947 | $pos = strpos($def, "\n");
2948 | $def = trim($def);
2949 |
2950 | if ($this->isLineWrapEnabled()) {
2951 | $def = str_replace("\n", $this->getLineBreak(), $def);
2952 | }
2953 |
2954 | if ($pos === 0) {
2955 | $def = '' . $def . ' ';
2956 | }
2957 |
2958 | if ($this->isLineWrapEnabled()) {
2959 | $term = str_replace("\n", $this->getLineBreak(), $term);
2960 | }
2961 |
2962 | $term = $this->graf($term);
2963 | $def = $this->graf($def);
2964 |
2965 | $out[] = "\t$term ";
2966 |
2967 | if ($def !== '') {
2968 | $out[] = "\t$def ";
2969 | }
2970 | }
2971 | }
2972 | }
2973 |
2974 | $out[] = ' ';
2975 | return implode("\n", $out);
2976 | }
2977 |
2978 | /**
2979 | * Parses Textile list structures into HTML.
2980 | *
2981 | * Searches for ordered, un-ordered and definition lists in the
2982 | * textile input and generates HTML lists for them.
2983 | *
2984 | * @param string $text The input
2985 | * @return string The parsed text
2986 | */
2987 | protected function textileLists($text)
2988 | {
2989 | return (string)preg_replace_callback(
2990 | "/^((?:[*;:]+|[*;:#]*#(?:_|\d+)?)$this->cls[ .].*)$(?![^#*;:])/smU",
2991 | array($this, "fTextileList"),
2992 | $text
2993 | );
2994 | }
2995 |
2996 | /**
2997 | * Constructs a HTML list from a Textile list structure.
2998 | *
2999 | * This method is used by Parser::textileLists() to process
3000 | * found list structures.
3001 | *
3002 | * @param array $m
3003 | * @return string HTML list
3004 | * @see Parser::textileLists()
3005 | */
3006 | protected function fTextileList($m)
3007 | {
3008 | $text = $m[0];
3009 | $lines = preg_split('/\n(?=[*#;:])/m', $m[0]);
3010 | $list = array();
3011 | $prev = false;
3012 | $out = array();
3013 | $lists = array();
3014 | $litem = '';
3015 |
3016 | if ($lines === false) {
3017 | return '';
3018 | }
3019 |
3020 | foreach ($lines as $line) {
3021 | $match = preg_match(
3022 | "/^(?P[#*;:]+)(?P_|\d+)?(?P$this->cls)[ .](?P.*)$/s",
3023 | $line,
3024 | $m
3025 | );
3026 |
3027 | if ($match) {
3028 | $list[] = array_merge($m, array(
3029 | 'level' => strlen($m['tl']),
3030 | ));
3031 | } else {
3032 | $list[count($list) - 1]['content'] .= "\n" . $line;
3033 | }
3034 | }
3035 |
3036 | if (!$list || $list[0]['level'] > 1) {
3037 | return $text;
3038 | }
3039 |
3040 | foreach ($list as $index => $m) {
3041 | $start = '';
3042 | $content = trim((string) $m['content']);
3043 | $ltype = $this->liType((string) $m['tl']);
3044 |
3045 | if (isset($list[$index + 1])) {
3046 | $next = $list[$index + 1];
3047 | } else {
3048 | $next = false;
3049 | }
3050 |
3051 | if (strpos((string) $m['tl'], ';') !== false) {
3052 | $litem = 'dt';
3053 | } elseif (strpos((string) $m['tl'], ':') !== false) {
3054 | $litem = 'dd';
3055 | } else {
3056 | $litem = 'li';
3057 | }
3058 |
3059 | $showitem = ($content !== '');
3060 |
3061 | if ('o' === $ltype) {
3062 | if (!isset($this->olstarts[$m['tl']])) {
3063 | $this->olstarts[$m['tl']] = 1;
3064 | }
3065 |
3066 | if (!$prev || $m['level'] > $prev['level']) {
3067 | if ($m['st'] === '') {
3068 | $this->olstarts[$m['tl']] = 1;
3069 | } elseif ($m['st'] !== '_') {
3070 | $this->olstarts[$m['tl']] = (int) $m['st'];
3071 | }
3072 | }
3073 |
3074 | if ((!$prev || $m['level'] > $prev['level']) && $m['st'] !== '') {
3075 | $start = ' start="' . $this->olstarts[$m['tl']] . '"';
3076 | }
3077 |
3078 | if ($showitem) {
3079 | $this->olstarts[$m['tl']] += 1;
3080 | }
3081 | }
3082 |
3083 | if ($prev
3084 | && $prev['tl']
3085 | && strpos((string) $prev['tl'], ';') !== false
3086 | && strpos((string) $m['tl'], ':') !== false
3087 | ) {
3088 | $lists[$m['tl']] = 2;
3089 | }
3090 |
3091 | $tabs = str_repeat("\t", ((int) $m['level']) - 1);
3092 | $atts = $this->parseAttribs((string) $m['atts']);
3093 |
3094 | if (!isset($lists[$m['tl']])) {
3095 | $lists[$m['tl']] = 1;
3096 | $line = $tabs.'<'.$ltype.'l'.$atts.$start.'>';
3097 |
3098 | if ($showitem) {
3099 | $line .= "\n$tabs\t<$litem>$content";
3100 | }
3101 | } elseif ($showitem) {
3102 | $line = "$tabs\t<$litem$atts>$content";
3103 | } else {
3104 | $line = '';
3105 | }
3106 |
3107 | if ((!$next || $next['level'] <= $m['level']) && $showitem) {
3108 | $line .= "$litem>";
3109 | }
3110 |
3111 | foreach (array_reverse($lists) as $k => $v) {
3112 | $indent = strlen((string) $k);
3113 |
3114 | if (!$next || $indent > $next['level']) {
3115 | if ($v !== 2) {
3116 | $line .= "\n$tabs" . $this->liType((string) $k) . "l>";
3117 | }
3118 |
3119 | if ($v !== 2 && $indent > 1) {
3120 | $line .= "".$litem.">";
3121 | }
3122 |
3123 | unset($lists[$k]);
3124 | }
3125 | }
3126 |
3127 | $prev = $m;
3128 | $out[] = $line;
3129 | }
3130 |
3131 | $out = implode("\n", $out);
3132 | return $this->doTagBr($litem, $out);
3133 | }
3134 |
3135 | /**
3136 | * Determines the list type from the Textile input symbol.
3137 | *
3138 | * @param string $in Textile input containing the possible list marker
3139 | * @return string Either 'd', 'o', 'u'
3140 | */
3141 | protected function liType($in)
3142 | {
3143 | $m = array();
3144 | $type = 'd';
3145 | if (preg_match('/^(?P[#*]+)/', $in, $m)) {
3146 | $type = ('#' === substr($m['type'], -1)) ? 'o' : 'u';
3147 | }
3148 | return $type;
3149 | }
3150 |
3151 | /**
3152 | * Adds br tags within the specified container tag.
3153 | *
3154 | * @param string $tag The tag
3155 | * @param string $in The input
3156 | * @return string
3157 | */
3158 | protected function doTagBr($tag, $in)
3159 | {
3160 | return (string)preg_replace_callback(
3161 | '@<(?P'.preg_quote($tag).')(?P[^>]*?)>(?P.*)(?P\1>)@s',
3162 | array($this, 'fBr'),
3163 | $in
3164 | );
3165 | }
3166 |
3167 | /**
3168 | * Adds br tags to paragraphs and headings.
3169 | *
3170 | * @param string $in The input
3171 | * @return string
3172 | */
3173 | protected function doPBr($in)
3174 | {
3175 | return (string)preg_replace_callback(
3176 | '@<(?Pp|h[1-6])(?P[^>]*?)>(?P.*)(?P\1>)@s',
3177 | array($this, 'fPBr'),
3178 | $in
3179 | );
3180 | }
3181 |
3182 | /**
3183 | * Less restrictive version of fBr method.
3184 | *
3185 | * Used only in paragraphs and headings where the next row may
3186 | * start with a smiley or perhaps something like '#8 bolt...'
3187 | * or '*** stars...'.
3188 | *
3189 | * @param array $m The input
3190 | * @return string
3191 | */
3192 | protected function fPBr($m)
3193 | {
3194 | if ($this->isLineWrapEnabled()) {
3195 | // Replaces \n instances that are not followed by white-space,
3196 | // or at end, with single LF.
3197 | $m['content'] = preg_replace(
3198 | "~ {$this->regex_snippets['space']}*\n(?![{$this->regex_snippets['space']}|])~i",
3199 | "\n",
3200 | (string) $m['content']
3201 | );
3202 | }
3203 |
3204 | // Replaces those LFs that aren't followed by white-space, or at end, with or a space.
3205 | $m['content'] = preg_replace(
3206 | "/\n(?![\s|])/",
3207 | $this->isLineWrapEnabled() ? $this->getLineBreak() : ' ',
3208 | (string) $m['content']
3209 | );
3210 |
3211 | return '<'.$m['tag'].$m['atts'].'>'.$m['content'].$m['closetag'];
3212 | }
3213 |
3214 | /**
3215 | * Formats line breaks.
3216 | *
3217 | * @param array $m The input
3218 | * @return string
3219 | */
3220 | protected function fBr($m)
3221 | {
3222 | $content = preg_replace(
3223 | "@(.+)(?| |||)\n(?![\s|])@",
3224 | $this->isLineWrapEnabled() ? '$1'.$this->getLineBreak() : '$1 ',
3225 | $m['content']
3226 | );
3227 |
3228 | return '<'.$m['tag'].$m['atts'].'>'.$content.$m['closetag'];
3229 | }
3230 |
3231 | /**
3232 | * Splits the given input into blocks.
3233 | *
3234 | * Blocks are separated by double line-break boundaries, and processed
3235 | * the blocks one by one.
3236 | *
3237 | * @param string $text Textile source text
3238 | * @return string Input text with blocks processed
3239 | */
3240 | protected function blocks($text)
3241 | {
3242 | $regex = '/^(?P'.join('|', $this->blocktag_whitelist).')'.
3243 | '(?P'.$this->a.$this->cls.$this->a.')\.(?P\.?)(?::(?P\S+))? (?P.*)$/Ss'.
3244 | $this->regex_snippets['mod'];
3245 |
3246 | $textblocks = preg_split('/(\n{2,})/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
3247 |
3248 | if ($textblocks === false) {
3249 | return '';
3250 | }
3251 |
3252 | $eatWhitespace = false;
3253 | $whitespace = '';
3254 | $ext = '';
3255 | $out = array();
3256 | $c1 = '';
3257 |
3258 | foreach ($textblocks as $block) {
3259 | // Line is just whitespace, keep it for the next block.
3260 | if (trim($block) === '') {
3261 | if ($eatWhitespace === false) {
3262 | $whitespace .= $block;
3263 | }
3264 | continue;
3265 | }
3266 |
3267 | // @phpstan-ignore-next-line
3268 | if (!$ext) {
3269 | $tag = 'p';
3270 | $atts = '';
3271 | $cite = '';
3272 | $eat = false;
3273 | }
3274 |
3275 | $eatWhitespace = false;
3276 | $anonymous_block = !preg_match($regex, $block, $m);
3277 |
3278 | if (!$anonymous_block) {
3279 | // Last block was extended, so close it
3280 | // @phpstan-ignore-next-line
3281 | if ($ext) {
3282 | $out[count($out)-1] .= $c1;
3283 | }
3284 |
3285 | // Extract the new block's parts
3286 | extract($m);
3287 | list($o1, $o2, $content, $c2, $c1, $eat) = $this->fBlock($m);
3288 |
3289 | // Leave off c1 if this block is extended, we'll close it at the start of the next block
3290 | $block = $o1.$o2.$content.$c2;
3291 |
3292 | // @phpstan-ignore-next-line
3293 | if (!$ext) {
3294 | $block .= $c1;
3295 | }
3296 | } else {
3297 | $rawBlock = preg_match($this->patterns['divider'], $block) ||
3298 | ($this->isRawBlocksEnabled() && $this->isRawBlock($block));
3299 |
3300 | // @phpstan-ignore-next-line
3301 | if ($ext || (strpos($block, ' ') !== 0 && !$rawBlock)) {
3302 | list($o1, $o2, $content, $c2, $c1, $eat) = $this->fBlock(array(
3303 | '',
3304 | $tag,
3305 | $atts,
3306 | $ext,
3307 | $cite,
3308 | $block,
3309 | ));
3310 |
3311 | // Skip $o1/$c1 because this is part of a continuing extended block
3312 | // @phpstan-ignore-next-line
3313 | if ($tag == 'p' && !$this->hasRawText($content)) {
3314 | $block = $content;
3315 | } else {
3316 | $block = $o2.$content.$c2;
3317 | }
3318 | } elseif ($rawBlock && $this->isRestrictedModeEnabled()) {
3319 | $block = $this->shelve($this->rEncodeHTML($block));
3320 | } elseif ($rawBlock) {
3321 | $block = $this->shelve($block);
3322 | } else {
3323 | $block = $this->graf($block);
3324 | }
3325 | }
3326 |
3327 | $block = $whitespace . $this->doPBr((string) $block);
3328 | if ($this->getDocumentType() === self::DOCTYPE_XHTML) {
3329 | $block = str_replace(' ', ' ', $block);
3330 | }
3331 |
3332 | // @phpstan-ignore-next-line
3333 | if ($ext && $anonymous_block) {
3334 | $out[count($out)-1] .= $block;
3335 | } elseif (!$eat) {
3336 | $out[] = $block;
3337 | }
3338 |
3339 | if ($eat) {
3340 | $eatWhitespace = true;
3341 | } else {
3342 | $whitespace = '';
3343 | }
3344 | }
3345 |
3346 | // @phpstan-ignore-next-line
3347 | if ($ext) {
3348 | $out[count($out)-1] .= $c1;
3349 | }
3350 |
3351 | return join('', $out);
3352 | }
3353 |
3354 | /**
3355 | * Formats the given block.
3356 | *
3357 | * Adds block tags and formats the text content inside
3358 | * the block.
3359 | *
3360 | * @param array $m The block content to format
3361 | * @return array
3362 | */
3363 | protected function fBlock($m)
3364 | {
3365 | list(, $tag, $att, $ext, $cite, $content) = $m;
3366 | $atts = $this->parseAttribs($att);
3367 | $space = $this->regex_snippets['space'];
3368 |
3369 | $o1 = '';
3370 | $o2 = '';
3371 | $c2 = '';
3372 | $c1 = '';
3373 | $eat = false;
3374 |
3375 | if ($tag === 'p') {
3376 | // Is this an anonymous block with a note definition?
3377 | $notedef = preg_replace_callback(
3378 | "/
3379 | ^note\# # start of note def marker
3380 | (?P[^%<*!@#^([{ {$space}.]+) # label
3381 | (?P [*!^]?) # link
3382 | (?P{$this->cls}) # att
3383 | \.? # optional period.
3384 | {$space}+ # whitespace ends def marker
3385 | (?P.*)$ # content
3386 | /x".$this->regex_snippets['mod'],
3387 | array($this, "fParseNoteDefs"),
3388 | $content
3389 | );
3390 |
3391 | if ($notedef === '' || $notedef === null) {
3392 | // It will be empty if the regex matched and ate it.
3393 | return array($o1, $o2, $notedef, $c2, $c1, true);
3394 | }
3395 | }
3396 |
3397 | if (preg_match("/fn(?P{$this->regex_snippets['digit']}+)/".$this->regex_snippets['mod'], $tag, $fns)) {
3398 | $tag = 'p';
3399 | $fnid = empty($this->fn[$fns['fnid']]) ? $this->linkPrefix . ($this->linkIndex++) : $this->fn[$fns['fnid']];
3400 |
3401 | // If there is an author-specified ID goes on the wrapper & the auto-id gets pushed to the
3402 | $supp_id = '';
3403 | if (strpos($atts, 'class=') === false) {
3404 | $atts .= ' class="footnote"';
3405 | }
3406 |
3407 | if (strpos($atts, ' id=') === false) {
3408 | $atts .= ' id="fn' . $fnid . '"';
3409 | } else {
3410 | $supp_id = ' id="fn' . $fnid . '"';
3411 | }
3412 |
3413 | if (strpos($att, '^') === false) {
3414 | $sup = $this->formatFootnote($fns['fnid'], $supp_id);
3415 | } else {
3416 | $sup = $this->formatFootnote(''.$fns['fnid'] .' ', $supp_id);
3417 | }
3418 |
3419 | $content = $sup . ' ' . $content;
3420 | }
3421 |
3422 | if ($tag == "bq") {
3423 | $cite = $this->shelveURL($cite);
3424 | $cite = ($cite != '') ? ' cite="' . $cite . '"' : '';
3425 | $o1 = "\n";
3426 | $o2 = "\tparseAttribs($att, '', false).">";
3427 | $c2 = " ";
3428 | $c1 = "\n ";
3429 | } elseif ($tag == 'bc') {
3430 | $attrib_array = $this->parseAttribsToArray($att, 'code');
3431 | $code_class = '';
3432 | if (isset($attrib_array['lang'])) {
3433 | $code_class = ' class="'.$attrib_array['lang'].'"';
3434 | unset($attrib_array['lang']);
3435 | $atts = $this->formatAttributeString($attrib_array);
3436 | }
3437 | $o1 = "";
3438 | $c1 = " ";
3439 | $content = $this->shelve($this->rEncodeHTML($content));
3440 | } elseif ($tag == 'notextile') {
3441 | $content = $this->shelve($content);
3442 | $o1 = '';
3443 | $o2 = '';
3444 | $c1 = '';
3445 | $c2 = '';
3446 | } elseif ($tag == 'pre') {
3447 | $content = $this->shelve($this->rEncodeHTML($content));
3448 | $o1 = "";
3449 | $o2 = '';
3450 | $c2 = '';
3451 | $c1 = " ";
3452 | } elseif ($tag == '###') {
3453 | $eat = true;
3454 | } else {
3455 | $o2 = "<$tag$atts>";
3456 | $c2 = "$tag>";
3457 | }
3458 |
3459 | $content = (!$eat) ? $this->graf($content) : '';
3460 |
3461 | return array($o1, $o2, $content, $c2, $c1, $eat);
3462 | }
3463 |
3464 | /**
3465 | * Whether the block is a raw document node.
3466 | *
3467 | * Raw blocks will be shelved and left as is.
3468 | *
3469 | * @param string $text Block to check
3470 | * @return bool TRUE if the block is raw, FALSE otherwise
3471 | * @since 3.7.0
3472 | */
3473 | protected function isRawBlock($text)
3474 | {
3475 | if (preg_match($this->patterns['contained'], $text, $m)) {
3476 | if (preg_match($this->patterns['phrasing'], $m['open'])) {
3477 | return false;
3478 | }
3479 |
3480 | if (preg_match($this->patterns['block'], $m['open'])) {
3481 | return false;
3482 | }
3483 |
3484 | return true;
3485 | }
3486 |
3487 | return false;
3488 | }
3489 |
3490 | /**
3491 | * Formats a footnote.
3492 | *
3493 | * @param string $marker The marker
3494 | * @param string $atts Attributes
3495 | * @param bool $anchor TRUE, if its a reference link
3496 | * @return string Processed footnote
3497 | */
3498 | protected function formatFootnote($marker, $atts = '', $anchor = true)
3499 | {
3500 | $pattern = ($anchor) ? $this->symbols['fn_foot_pattern'] : $this->symbols['fn_ref_pattern'];
3501 | return $this->replaceMarkers((string) $pattern, array('atts' => $atts, 'marker' => $marker));
3502 | }
3503 |
3504 | /**
3505 | * Replaces markers with replacements in the given input.
3506 | *
3507 | * @param string $text The input
3508 | * @param array $replacements Marker replacement pairs
3509 | * @return string
3510 | */
3511 | protected function replaceMarkers($text, $replacements)
3512 | {
3513 | $map = array();
3514 |
3515 | foreach ($replacements as $from => $to) {
3516 | $map['{'.$from.'}'] = $to;
3517 | }
3518 |
3519 | return strtr($text, $map);
3520 | }
3521 |
3522 | /**
3523 | * Parses HTML comments in the given input.
3524 | *
3525 | * This method finds HTML comments in the given input
3526 | * and replaces them with reference tokens.
3527 | *
3528 | * @param string $text Textile input
3529 | * @return string $text Processed input
3530 | */
3531 | protected function getHTMLComments($text)
3532 | {
3533 | return (string)preg_replace_callback(
3534 | "/\/sx",
3535 | array($this, "fParseHTMLComments"),
3536 | $text
3537 | );
3538 | }
3539 |
3540 | /**
3541 | * Formats a HTML comment.
3542 | *
3543 | * Stores the comment on the shelf and returns
3544 | * a reference token wrapped in to a HTML comment.
3545 | *
3546 | * @param array $m Options
3547 | * @return string Reference token wrapped to a HTML comment tags
3548 | */
3549 | protected function fParseHTMLComments($m)
3550 | {
3551 | return '';
3552 | }
3553 |
3554 | /**
3555 | * Parses paragraphs in the given input.
3556 | *
3557 | * @param string $text Textile input
3558 | * @return string Processed input
3559 | */
3560 | protected function graf($text)
3561 | {
3562 | // Handle normal paragraph text
3563 | if (!$this->isLiteModeEnabled()) {
3564 | // Notextile blocks and inlines
3565 | $text = $this->noTextile($text);
3566 | // Handle code
3567 | $text = $this->code($text);
3568 | }
3569 |
3570 | // HTML comments --
3571 | $text = $this->getHTMLComments($text);
3572 | // Consume link aliases
3573 | $text = $this->getRefs($text);
3574 | // Treat quoted quote as a special glyph.
3575 | $text = $this->glyphQuotedQuote($text);
3576 | // Generate links
3577 | $text = $this->links($text);
3578 |
3579 | // Handle images (if permitted)
3580 | if ($this->isImageTagEnabled()) {
3581 | $text = $this->images($text);
3582 | }
3583 |
3584 | if (!$this->isLiteModeEnabled()) {
3585 | // Handle tables
3586 | $text = $this->tables($text);
3587 | // Handle redcloth-style definition lists
3588 | $text = $this->redclothLists($text);
3589 | // Handle ordered & unordered lists plus txp-style definition lists
3590 | $text = $this->textileLists($text);
3591 | }
3592 |
3593 | // Inline markup (em, strong, sup, sub, del etc)
3594 | $text = $this->spans($text);
3595 |
3596 | if (!$this->isLiteModeEnabled()) {
3597 | // Turn footnote references into supers or links.
3598 | // As footnote blocks are banned in lite mode there is no point
3599 | // generating links for them.
3600 | $text = $this->footnoteRefs($text);
3601 |
3602 | // Turn note references into links
3603 | $text = $this->noteRefs($text);
3604 | }
3605 |
3606 | // Glyph level substitutions (mainly typographic -- " & ' => curly quotes, -- => em-dash etc.
3607 | $text = $this->glyphs($text);
3608 |
3609 | return rtrim($text, "\n");
3610 | }
3611 |
3612 | /**
3613 | * Replaces Textile span tags with their equivalent HTML inline tags.
3614 | *
3615 | * @param string $text The Textile document to perform the replacements in
3616 | * @return string The Textile document with spans replaced by their HTML inline equivalents
3617 | */
3618 | protected function spans($text)
3619 | {
3620 | $span_tags = array_keys($this->span_tags);
3621 | $pnct = ".,\"'?!;:‹›«»„“”‚‘’";
3622 | $this->span_depth++;
3623 |
3624 | if ($this->span_depth <= $this->max_span_depth) {
3625 | foreach ($span_tags as $tag) {
3626 | $tag = preg_quote($tag);
3627 | $text = (string)preg_replace_callback(
3628 | "/
3629 | (?P^|(?<=[\s>$pnct\(])|[{[])
3630 | (?P$tag)(?!$tag)
3631 | (?P{$this->cls})
3632 | (?!$tag)
3633 | (?::(?P\S+[^$tag]{$this->regex_snippets['space']}))?
3634 | (?P[^{$this->regex_snippets['space']}$tag]+|\S.*?[^\s$tag\n])
3635 | (?P[$pnct]*)
3636 | $tag
3637 | (?P$|[\[\]}<]|(?=[$pnct]{1,2}[^0-9]|\s|\)))
3638 | /x".$this->regex_snippets['mod'],
3639 | array($this, "fSpan"),
3640 | $text
3641 | );
3642 | }
3643 | }
3644 | $this->span_depth--;
3645 | return $text;
3646 | }
3647 |
3648 | /**
3649 | * Formats a span tag and stores it on the shelf.
3650 | *
3651 | * @param array $m Options
3652 | * @return string Content wrapped to reference tokens
3653 | * @see Parser::spans()
3654 | */
3655 | protected function fSpan($m)
3656 | {
3657 | $m = $this->getSpecialOptions($m);
3658 | $tag = $this->span_tags[$m['tag']];
3659 | $atts = $this->parseAttribsToArray($m['atts']);
3660 |
3661 | if ($m['cite'] != '') {
3662 | $atts['cite'] = trim($m['cite']);
3663 | ksort($atts);
3664 | }
3665 |
3666 | $atts = $this->formatAttributeString($atts);
3667 | $content = $this->spans($m['content']);
3668 | $opentag = '<'.$tag.$atts.'>';
3669 | $closetag = ''.$tag.'>';
3670 | $tags = $this->storeTags($opentag, $closetag);
3671 | $out = "{$tags['open']}{$content}{$m['end']}{$tags['close']}";
3672 |
3673 | return $m['before'].$out.$m['after'];
3674 | }
3675 |
3676 | /**
3677 | * Stores a tag pair in the tag cache.
3678 | *
3679 | * @param string $opentag Opening tag
3680 | * @param string $closetag Closing tag
3681 | * @return array Reference tokens for both opening and closing tag
3682 | */
3683 | protected function storeTags($opentag, $closetag = '')
3684 | {
3685 | $tags = array();
3686 |
3687 | $this->refCache[$this->refIndex] = $opentag;
3688 | $tags['open'] = $this->uid.$this->refIndex.':ospan ';
3689 | $this->refIndex++;
3690 |
3691 | $this->refCache[$this->refIndex] = $closetag;
3692 | $tags['close'] = ' '.$this->uid.$this->refIndex.':cspan';
3693 | $this->refIndex++;
3694 |
3695 | return $tags;
3696 | }
3697 |
3698 | /**
3699 | * Replaces reference tokens with corresponding shelved span tags.
3700 | *
3701 | * This method puts all shelved span tags back to the final,
3702 | * parsed input.
3703 | *
3704 | * @param string $text The input
3705 | * @return string Processed text
3706 | * @see Parser::storeTags()
3707 | */
3708 | protected function retrieveTags($text)
3709 | {
3710 | $text = (string)preg_replace_callback(
3711 | '/'.$this->uid.'(?P[0-9]+):ospan /',
3712 | array($this, 'fRetrieveTags'),
3713 | $text
3714 | );
3715 |
3716 | $text = (string)preg_replace_callback(
3717 | '/ '.$this->uid.'(?P[0-9]+):cspan/',
3718 | array($this, 'fRetrieveTags'),
3719 | $text
3720 | );
3721 |
3722 | return $text;
3723 | }
3724 |
3725 | /**
3726 | * Retrieves a tag from the tag cache.
3727 | *
3728 | * @param array $m Options
3729 | * @return string
3730 | * @see Parser::retrieveTags()
3731 | */
3732 | protected function fRetrieveTags($m)
3733 | {
3734 | return $this->refCache[$m['token']];
3735 | }
3736 |
3737 | /**
3738 | * Parses note lists in the given input.
3739 | *
3740 | * This method should be ran after other blocks
3741 | * have been processed, but before reference tokens
3742 | * have been replaced with their replacements.
3743 | *
3744 | * @param string $text Textile input
3745 | * @return string Processed input
3746 | */
3747 | protected function placeNoteLists($text)
3748 | {
3749 | // Sequence all referenced definitions.
3750 | if ($this->notes) {
3751 | $o = array();
3752 | foreach ($this->notes as $label => $info) {
3753 | if (!empty($info['seq'])) {
3754 | // @phpstan-ignore-next-line
3755 | $o[$info['seq']] = $info;
3756 | $info['seq'] = $label;
3757 | } else {
3758 | // Unreferenced definitions go here for possible future use.
3759 | $this->unreferencedNotes[] = $info;
3760 | }
3761 | }
3762 |
3763 | if ($o) {
3764 | ksort($o);
3765 | }
3766 |
3767 | $this->notes = $o;
3768 | }
3769 |
3770 | // Replace list markers.
3771 | $text = (string)preg_replace_callback(
3772 | '@notelist(?P'.$this->c.')'.
3773 | '(?:\:(?P['.$this->regex_snippets['wrd'].'|'.$this->syms.']))?'.
3774 | '(?P[\^!]?)(?P\+?)\.?'.$this->regex_snippets['space'].'* @U'.
3775 | $this->regex_snippets['mod'],
3776 | array($this, "fNoteLists"),
3777 | $text
3778 | );
3779 |
3780 | return $text;
3781 | }
3782 |
3783 | /**
3784 | * Formats a note list.
3785 | *
3786 | * @param array $m Options
3787 | * @return string Processed note list
3788 | */
3789 | protected function fNoteLists($m)
3790 | {
3791 | if (!$m['startchar']) {
3792 | $m['startchar'] = 'a';
3793 | }
3794 |
3795 | $index = $m['links'].$m['extras'].$m['startchar'];
3796 |
3797 | if (empty($this->notelist_cache[$index])) {
3798 | // If not in cache, build the entry
3799 | $out = array();
3800 |
3801 | if ($this->notes) {
3802 | foreach ($this->notes as $seq => $info) {
3803 | $links = $this->makeBackrefLink($info, $m['links'], $m['startchar']);
3804 |
3805 | if (!empty($info['def'])) {
3806 | // @phpstan-ignore-next-line
3807 | $out[] = "\t".''.$links.
3808 | ' '.$info['def']['content'].' ';
3809 | } else {
3810 | // @phpstan-ignore-next-line
3811 | $out[] = "\t".''.$links.' Undefined Note [#'.$info['seq'].']. ';
3812 | }
3813 | }
3814 | }
3815 |
3816 | if ('+' == $m['extras'] && $this->unreferencedNotes) {
3817 | foreach ($this->unreferencedNotes as $info) {
3818 | if (!empty($info['def'])) {
3819 | // @phpstan-ignore-next-line
3820 | $out[] = "\t".''.$info['def']['content'].' ';
3821 | }
3822 | }
3823 | }
3824 |
3825 | $this->notelist_cache[$index] = join("\n", $out);
3826 | }
3827 |
3828 | if ($this->notelist_cache[$index]) {
3829 | $atts = $this->parseAttribs($m['atts']);
3830 | return "\n{$this->notelist_cache[$index]}\n ";
3831 | }
3832 |
3833 | return '';
3834 | }
3835 |
3836 | /**
3837 | * Renders a note back reference link.
3838 | *
3839 | * This method renders an array of back reference
3840 | * links for notes.
3841 | *
3842 | * @param array|int|string> $info Options
3843 | * @param string $g_links Reference type
3844 | * @param string $i Instance count
3845 | * @return string Processed input
3846 | */
3847 | protected function makeBackrefLink($info, $g_links, $i)
3848 | {
3849 | $backlink_type = !empty($info['def']) && $info['def']['link'] ? $info['def']['link'] : $g_links;
3850 | $allow_inc = (false === strpos($this->syms, $i));
3851 |
3852 | $i_ = str_replace(array('&', ';', '#'), '', $this->encodeHigh($i));
3853 | $decode = (strlen($i) !== strlen($i_));
3854 |
3855 | if ($backlink_type === '!') {
3856 | return '';
3857 | } elseif ($backlink_type === '^') {
3858 | return ''.$i.' ';
3859 | } else {
3860 | $out = array();
3861 |
3862 | /** @var array $items */
3863 | $items = $info['refids'];
3864 |
3865 | foreach ($items as $id) {
3866 | $out[] = ''. (($decode) ? $this->decodeHigh($i_) : $i_) .' ';
3867 | if ($allow_inc) {
3868 | $i_++;
3869 | }
3870 | }
3871 |
3872 | return join(' ', $out);
3873 | }
3874 | }
3875 |
3876 | /**
3877 | * Formats note definitions.
3878 | *
3879 | * This method formats notes and stores them in
3880 | * note cache for later use and to build reference
3881 | * links.
3882 | *
3883 | * @param array $m Options
3884 | * @return string Empty string
3885 | */
3886 | protected function fParseNoteDefs($m)
3887 | {
3888 | $label = $m['label'];
3889 | $link = $m['link'];
3890 | $att = $m['att'];
3891 | $content = $m['content'];
3892 |
3893 | // Assign an id if the note reference parse hasn't found the label yet.
3894 | if (empty($this->notes[$label]['id'])) {
3895 | $this->notes[$label]['id'] = $this->linkPrefix . ($this->linkIndex++);
3896 | }
3897 |
3898 | // Ignores subsequent defs using the same label
3899 | if (empty($this->notes[$label]['def'])) {
3900 | $this->notes[$label]['def'] = array(
3901 | 'atts' => $this->parseAttribs($att),
3902 | 'content' => $this->graf($content),
3903 | 'link' => $link,
3904 | );
3905 | }
3906 | return '';
3907 | }
3908 |
3909 | /**
3910 | * Parses note references in the given input.
3911 | *
3912 | * This method replaces note reference tags with
3913 | * links.
3914 | *
3915 | * @param string $text Textile input
3916 | * @return string
3917 | */
3918 | protected function noteRefs($text)
3919 | {
3920 | return (string)preg_replace_callback(
3921 | "/\[(?P{$this->c})\#(?P[^\]!]+?)(?P[!]?)\]/Ux",
3922 | array($this, "fParseNoteRefs"),
3923 | $text
3924 | );
3925 | }
3926 |
3927 | /**
3928 | * Formats note reference links.
3929 | *
3930 | * By the time this function is called, all note lists will have been
3931 | * processed into the notes array, and we can resolve the link numbers in
3932 | * the order we process the references.
3933 | *
3934 | * @param array $m Options
3935 | * @return string Note reference
3936 | */
3937 | protected function fParseNoteRefs($m)
3938 | {
3939 | $atts = $this->parseAttribs($m['atts']);
3940 | $nolink = ($m['nolink'] === '!');
3941 |
3942 | // Assign a sequence number to this reference if there isn't one already.
3943 |
3944 | if (empty($this->notes[$m['label']]['seq'])) {
3945 | $num = $this->notes[$m['label']]['seq'] = ($this->note_index++);
3946 | } else {
3947 | /** @var int $num */
3948 | $num = $this->notes[$m['label']]['seq'];
3949 | }
3950 |
3951 | // Make our anchor point & stash it for possible use in backlinks when the
3952 | // note list is generated later.
3953 | $refid = $this->linkPrefix . ($this->linkIndex++);
3954 |
3955 | // @phpstan-ignore-next-line
3956 | $this->notes[$m['label']]['refids'][] = $refid;
3957 |
3958 | // If we are referencing a note that hasn't had the definition parsed yet, then assign it an ID.
3959 |
3960 | if (empty($this->notes[$m['label']]['id'])) {
3961 | // @phpstan-ignore-next-line
3962 | $id = $this->notes[$m['label']]['id'] = $this->linkPrefix . ($this->linkIndex++);
3963 | } else {
3964 | $id = $this->notes[$m['label']]['id'];
3965 | }
3966 |
3967 | // Build the link (if any).
3968 | $out = ''.$num.' ';
3969 |
3970 | if (!$nolink) {
3971 | // @phpstan-ignore-next-line
3972 | $out = ''.$out.' ';
3973 | }
3974 |
3975 | // Build the reference.
3976 | return $this->replaceMarkers((string) $this->symbols['nl_ref_pattern'], array(
3977 | 'atts' => $atts,
3978 | 'marker' => $out,
3979 | ));
3980 | }
3981 |
3982 | /**
3983 | * Parses URI into component parts.
3984 | *
3985 | * This method splits a URI-like string apart into component parts, while
3986 | * also providing validation.
3987 | *
3988 | * @param string $uri The string to pick apart (if possible)
3989 | * @param array $m Reference to an array the URI component parts are assigned to
3990 | * @return bool TRUE if the string validates as a URI
3991 | * @link http://tools.ietf.org/html/rfc3986#appendix-B
3992 | */
3993 | protected function parseURI($uri, &$m)
3994 | {
3995 | $r = "@^((?P[^:/?#]+):)?".
3996 | "(//(?P[^/?#]*))?".
3997 | "(?P[^?#]*)".
3998 | "(\?(?P[^#]*))?".
3999 | "(#(?P.*))?@";
4000 |
4001 | return preg_match($r, $uri, $m) === 1;
4002 | }
4003 |
4004 | /**
4005 | * Checks whether a component part can be added to a URI.
4006 | *
4007 | * @param array $mask An array of allowed component parts
4008 | * @param string $name The component to add
4009 | * @param array $parts An array of existing components to modify
4010 | * @return bool TRUE if the component can be added
4011 | */
4012 | protected function addPart($mask, $name, $parts)
4013 | {
4014 | return (in_array($name, $mask) && isset($parts[$name]) && '' !== $parts[$name]);
4015 | }
4016 |
4017 | /**
4018 | * Rebuild a URI from parsed parts and a mask.
4019 | *
4020 | * @param array $parts Full array of URI parts
4021 | * @param string $mask Comma separated list of URI parts to include in the rebuilt URI
4022 | * @param bool $encode Flag to control encoding of the path part of the rebuilt URI
4023 | * @return string The rebuilt URI
4024 | * @link http://tools.ietf.org/html/rfc3986#section-5.3
4025 | */
4026 | protected function rebuildURI($parts, $mask = 'scheme,authority,path,query,fragment', $encode = true)
4027 | {
4028 | $mask = explode(',', $mask);
4029 | $out = '';
4030 |
4031 | if ($this->addPart($mask, 'scheme', $parts)) {
4032 | $out .= $parts['scheme'] . ':';
4033 | }
4034 |
4035 | if ($this->addPart($mask, 'authority', $parts)) {
4036 | $out .= '//' . $parts['authority'];
4037 | }
4038 |
4039 | if ($this->addPart($mask, 'path', $parts)) {
4040 | if (!$encode) {
4041 | $out .= $parts['path'];
4042 | } else {
4043 | $pp = explode('/', $parts['path']);
4044 | foreach ($pp as &$p) {
4045 | $p = str_replace(array('%25', '%40'), array('%', '@'), rawurlencode($p));
4046 | if (!in_array($parts['scheme'], array('mailto'))) {
4047 | $p = str_replace('%2B', '+', $p);
4048 | }
4049 | }
4050 |
4051 | $pp = implode('/', $pp);
4052 | $out .= $pp;
4053 | }
4054 | }
4055 |
4056 | if ($this->addPart($mask, 'query', $parts)) {
4057 | $out .= '?' . $parts['query'];
4058 | }
4059 |
4060 | if ($this->addPart($mask, 'fragment', $parts)) {
4061 | $out .= '#' . $parts['fragment'];
4062 | }
4063 |
4064 | return $out;
4065 | }
4066 |
4067 | /**
4068 | * Parses and shelves links in the given input.
4069 | *
4070 | * This method parses the input Textile document for links.
4071 | * Formats and encodes them, and stores the created link
4072 | * elements in cache.
4073 | *
4074 | * @param string $text Textile input
4075 | * @return string The input document with link pulled out and replaced with tokens
4076 | */
4077 | protected function links($text)
4078 | {
4079 | $text = $this->markStartOfLinks($text);
4080 | return $this->replaceLinks($text);
4081 | }
4082 |
4083 | /**
4084 | * Finds and marks the start of well formed links in the input text.
4085 | *
4086 | * @param string $text String to search for link starting positions
4087 | * @return string Text with links marked
4088 | * @see Parser::links()
4089 | */
4090 | protected function markStartOfLinks($text)
4091 | {
4092 | // Slice text on '":' boundaries. These always occur in inline
4093 | // links between the link text and the url part and are much more
4094 | // infrequent than '"' characters so we have less possible links
4095 | // to process.
4096 | $mod = $this->regex_snippets['mod'];
4097 | $slices = preg_split('/":(?='.$this->regex_snippets['char'].')/'.$mod, $text);
4098 |
4099 | if ($slices === false) {
4100 | return '';
4101 | }
4102 |
4103 | if (count($slices) > 1) {
4104 | // There are never any start of links in the last slice, so pop it
4105 | // off (we'll glue it back later).
4106 | $last_slice = array_pop($slices);
4107 |
4108 | foreach ($slices as &$slice) {
4109 | // If there is no possible start quote then this slice is not a link
4110 | if (strpos($slice, '"') === false) {
4111 | continue;
4112 | }
4113 |
4114 | // Cut this slice into possible starting points wherever we
4115 | // find a '"' character. Any of these parts could represent
4116 | // the start of the link text - we have to find which one.
4117 | $possible_start_quotes = explode('"', $slice);
4118 |
4119 | // Start our search for the start of the link with the closest prior
4120 | // quote mark.
4121 | $possibility = rtrim(array_pop($possible_start_quotes));
4122 |
4123 | // Init the balanced count. If this is still zero at the end
4124 | // of our do loop we'll mark the " that caused it to balance
4125 | // as the start of the link and move on to the next slice.
4126 | $balanced = 0;
4127 | $linkparts = array();
4128 | $iter = 0;
4129 |
4130 | while ($possibility !== null) {
4131 | // Starting at the end, pop off the previous part of the
4132 | // slice's fragments.
4133 |
4134 | // Add this part to those parts that make up the link text.
4135 | $linkparts[] = $possibility;
4136 |
4137 | if ($possibility !== '') {
4138 | // did this part inc or dec the balanced count?
4139 | if (preg_match('/^\S|=$/'.$mod, $possibility)) {
4140 | $balanced--;
4141 | }
4142 |
4143 | if (preg_match('/\S$/'.$mod, $possibility)) {
4144 | $balanced++;
4145 | }
4146 |
4147 | $possibility = array_pop($possible_start_quotes);
4148 | } else {
4149 | // If quotes occur next to each other, we get zero length strings.
4150 | // eg. ...""Open the door, HAL!"":url...
4151 | // In this case we count a zero length in the last position as a
4152 | // closing quote and others as opening quotes.
4153 | $balanced = (!$iter++) ? $balanced+1 : $balanced-1;
4154 |
4155 | $possibility = array_pop($possible_start_quotes);
4156 |
4157 | // If out of possible starting segments we back the last one
4158 | // from the linkparts array
4159 | if ($possibility === null) {
4160 | array_pop($linkparts);
4161 | break;
4162 | }
4163 |
4164 | // If the next possibility is empty or ends in a space we have a
4165 | // closing ".
4166 | if ($possibility === '' ||
4167 | preg_match("~{$this->regex_snippets['space']}$~".$mod, $possibility)) {
4168 | $balanced = 0; // force search exit
4169 | }
4170 | }
4171 |
4172 | if ($balanced <= 0) {
4173 | array_push($possible_start_quotes, $possibility);
4174 | break;
4175 | }
4176 | }
4177 |
4178 | // Rebuild the link's text by reversing the parts and sticking them back
4179 | // together with quotes.
4180 | $link_content = implode('"', array_reverse($linkparts));
4181 |
4182 | // Rebuild the remaining stuff that goes before the link but that's
4183 | // already in order.
4184 | $pre_link = implode('"', $possible_start_quotes);
4185 |
4186 | // Re-assemble the link starts with a specific marker for the next regex.
4187 | $slice = $pre_link . $this->uid.'linkStartMarker:"' . $link_content;
4188 | }
4189 |
4190 | // Add the last part back
4191 | $slices[] = $last_slice;
4192 | }
4193 |
4194 | // Re-assemble the full text with the start and end markers
4195 | $text = implode('":', $slices);
4196 |
4197 | return $text;
4198 | }
4199 |
4200 | /**
4201 | * Replaces links with tokens and stores them on the shelf.
4202 | *
4203 | * @param string $text The input
4204 | * @return string Processed input
4205 | * @see Parser::links()
4206 | */
4207 | protected function replaceLinks($text)
4208 | {
4209 | $stopchars = "\s|^'\"*";
4210 | $needle = $this->uid . 'linkStartMarker:';
4211 | $prev = null;
4212 |
4213 | while (\strpos($text, $needle) !== false) {
4214 | $text = (string)preg_replace_callback(
4215 | '/
4216 | (?P\[)?
4217 | ' . $needle . '"
4218 | (?P(?:.|\n)*?)
4219 | ":(?P[^' . $stopchars . ']*)
4220 | /x' . $this->regex_snippets['mod'],
4221 | array($this, "fLink"),
4222 | $text
4223 | );
4224 |
4225 | if ($prev === $text) {
4226 | break;
4227 | }
4228 |
4229 | $prev = $text;
4230 | }
4231 |
4232 | return $text;
4233 | }
4234 |
4235 | /**
4236 | * Formats a link and stores it on the shelf.
4237 | *
4238 | * @param array $m Options
4239 | * @return string Reference token for the shelved content
4240 | * @see Parser::replaceLinks()
4241 | */
4242 | protected function fLink($m)
4243 | {
4244 | $in = $m[0];
4245 | $pre = $m['pre'];
4246 | if ($this->isLineWrapEnabled()) {
4247 | $inner = str_replace("\n", $this->getLineBreak(), $m['inner']);
4248 | } else {
4249 | $inner = str_replace("\n", ' ', $m['inner']);
4250 | }
4251 | $url = $m['urlx'];
4252 | $m = array();
4253 |
4254 | // Treat empty inner part as an invalid link.
4255 | if (trim($inner) === '') {
4256 | return $pre.'"'.$inner.'":'.$url;
4257 | }
4258 |
4259 | // Split inner into $atts, $text and $title..
4260 | preg_match(
4261 | '/
4262 | ^
4263 | (?P' . $this->cls . ') # $atts (if any)
4264 | ' . $this->regex_snippets['space'] . '* # any optional spaces
4265 | (?P # $text is...
4266 | (!.+!) # an image
4267 | | # else...
4268 | .+? # link text
4269 | ) # end of $text
4270 | (?:\((?P[^)]+?)\))? # $title (if any)
4271 | $
4272 | /x'.$this->regex_snippets['mod'],
4273 | $inner,
4274 | $m
4275 | );
4276 | $atts = isset($m['atts']) ? $m['atts'] : '';
4277 | $text = isset($m['text']) ? trim($m['text']) : $inner;
4278 | $title = isset($m['title']) ? $m['title'] : '';
4279 | $m = array();
4280 |
4281 | $pop = $tight = '';
4282 | $counts = array(
4283 | '[' => null,
4284 | ']' => substr_count($url, ']'), # We need to know how many closing square brackets we have
4285 | '(' => null,
4286 | ')' => null,
4287 | );
4288 |
4289 | // Look for footnotes or other square-bracket delimieted stuff at the end of the url...
4290 | // eg. "text":url][otherstuff... will have "[otherstuff" popped back out.
4291 | // "text":url?q[]=x][123] will have "[123]" popped off the back, the remaining closing square brackets
4292 | // will later be tested for balance
4293 | if ($counts[']']
4294 | && 1 === preg_match('@(?P^.*\])(?P\[.*?)$@' . $this->regex_snippets['mod'], $url, $m)
4295 | ) {
4296 | $url = $m['url'];
4297 | $tight = $m['tight'];
4298 | $m = array();
4299 | }
4300 |
4301 | // Split off any trailing text that isn't part of an array assignment.
4302 | // eg. "text":...?q[]=value1&q[]=value2 ... is ok
4303 | // "text":...?q[]=value1]following ... would have "following"
4304 | // popped back out and the remaining square bracket
4305 | // will later be tested for balance
4306 | if ($counts[']']
4307 | && 1 === preg_match('@(?P^.*\])(?!=)(?P.*?)$@' . $this->regex_snippets['mod'], $url, $m)
4308 | ) {
4309 | $url = $m['url'];
4310 | $tight = $m['end'] . $tight;
4311 | $m = array();
4312 | }
4313 |
4314 | // Does this need to be mb_ enabled? We are only searching for text in the ASCII charset anyway
4315 | // Create an array of (possibly) multi-byte characters.
4316 | // This is going to allow us to pop off any non-matched or nonsense chars from the url
4317 | $url_chars = str_split($url);
4318 |
4319 | // Now we have the array of all the multi-byte chars in the url we will parse the
4320 | // uri backwards and pop off
4321 | // any chars that don't belong there (like . or , or unmatched brackets of various kinds).
4322 | $first = true;
4323 | do {
4324 | $c = array_pop($url_chars);
4325 | $popped = false;
4326 | switch ($c) {
4327 | // Textile URL shouldn't end in these characters, we pop
4328 | // them off the end and push them out the back of the url again.
4329 | case '!':
4330 | case '?':
4331 | case ':':
4332 | case ';':
4333 | case '.':
4334 | case ',':
4335 | $pop = $c . $pop;
4336 | $popped = true;
4337 | break;
4338 |
4339 | case '>':
4340 | $urlLeft = implode('', $url_chars);
4341 |
4342 | if (preg_match('@(?P<\/[a-z]+)$@', $urlLeft, $m)) {
4343 | $url_chars = str_split(substr($urlLeft, 0, strlen($m['tag']) * -1));
4344 | $pop = $m['tag'] . $c . $pop;
4345 | $popped = true;
4346 | }
4347 |
4348 | break;
4349 |
4350 | case ']':
4351 | // If we find a closing square bracket we are going to see if it is balanced.
4352 | // If it is balanced with matching opening bracket then it is part of the URL
4353 | // else we spit it back out of the URL.
4354 | if (null === $counts['[']) {
4355 | $counts['['] = substr_count($url, '[');
4356 | }
4357 |
4358 | if ($counts['['] === $counts[']']) {
4359 | // It is balanced, so keep it
4360 | $url_chars[] = $c;
4361 | } else {
4362 | // In the case of un-matched closing square brackets we just eat it
4363 | $popped = true;
4364 | $counts[']'] -= 1;
4365 | if ($first) {
4366 | $pre = '';
4367 | }
4368 | }
4369 | break;
4370 |
4371 | case ')':
4372 | if (null === $counts[')']) {
4373 | $counts['('] = substr_count($url, '(');
4374 | $counts[')'] = substr_count($url, ')');
4375 | }
4376 |
4377 | if ($counts['('] === $counts[')']) {
4378 | // It is balanced, so keep it
4379 | $url_chars[] = $c;
4380 | } else {
4381 | // Unbalanced so spit it out the back end
4382 | $pop = $c . $pop;
4383 | $counts[')'] -= 1;
4384 | $popped = true;
4385 | }
4386 | break;
4387 |
4388 | default:
4389 | // We have an acceptable character for the end of the url so put it back and
4390 | // exit the character popping loop
4391 | $url_chars[] = $c;
4392 | break;
4393 | }
4394 | $first = false;
4395 | } while ($popped);
4396 |
4397 | $url = implode('', $url_chars);
4398 | $uri_parts = array();
4399 | $this->parseURI($url, $uri_parts);
4400 |
4401 | if (!$this->isValidUrl($url)) {
4402 | return str_replace($this->uid.'linkStartMarker:', '', $in);
4403 | }
4404 |
4405 | $scheme = $uri_parts['scheme'];
4406 | $scheme_in_list = in_array($scheme, $this->url_schemes);
4407 |
4408 | if ('$' === $text) {
4409 | if ($scheme_in_list) {
4410 | $text = ltrim($this->rebuildURI($uri_parts, 'authority,path,query,fragment', false), '/');
4411 | } else {
4412 | if (isset($this->urlrefs[$url])) {
4413 | $url = urldecode($this->urlrefs[$url]);
4414 | }
4415 |
4416 | $text = $url;
4417 | }
4418 | }
4419 |
4420 | $text = trim($text);
4421 | $title = $this->encodeHTML($title);
4422 |
4423 | if ($this->isImageTagEnabled()) {
4424 | $text = $this->images($text);
4425 | }
4426 |
4427 | $text = $this->spans($text);
4428 | $text = $this->glyphs($text);
4429 | $url = $this->shelveURL($this->rebuildURI($uri_parts));
4430 | $a = $this->newTag(
4431 | 'a',
4432 | $this->parseAttribsToArray($atts),
4433 | false
4434 | )->title($title)->href($url, true)->rel($this->rel);
4435 | $tags = $this->storeTags((string) $a, '');
4436 | $out = $this->shelve($tags['open'].trim($text).$tags['close']);
4437 |
4438 | return $pre . $out . $pop . $tight;
4439 | }
4440 |
4441 | /**
4442 | * Finds URI aliases within the given input.
4443 | *
4444 | * This method finds URI aliases in the Textile input. Links are stored
4445 | * in an internal cache, so that they can be referenced from any link
4446 | * in the document.
4447 | *
4448 | * This operation happens before the actual link parsing takes place.
4449 | *
4450 | * @param string $text Textile input
4451 | * @return string The Textile document with any URI aliases removed
4452 | */
4453 | protected function getRefs($text)
4454 | {
4455 | $pattern = array();
4456 |
4457 | foreach ($this->url_schemes as $scheme) {
4458 | $pattern[] = preg_quote($scheme.':', '/');
4459 | }
4460 |
4461 | $pattern =
4462 | '/^\[(?P.+)\]'.
4463 | '(?P(?:'.join('|', $pattern).'|\/)\S+)'.
4464 | '(?='.$this->regex_snippets['space'].'|$)/Um';
4465 |
4466 | return (string)preg_replace_callback(
4467 | $pattern.$this->regex_snippets['mod'],
4468 | array($this, "refs"),
4469 | $text
4470 | );
4471 | }
4472 |
4473 | /**
4474 | * Parses, encodes and shelves the current URI alias.
4475 | *
4476 | * @param array $m Options
4477 | * @return string Empty string
4478 | * @see Parser::getRefs()
4479 | */
4480 | protected function refs($m)
4481 | {
4482 | $uri_parts = array();
4483 | $this->parseURI($m['url'], $uri_parts);
4484 | // Encodes URL if needed.
4485 | $this->urlrefs[$m['alias']] = ltrim($this->rebuildURI($uri_parts));
4486 | return '';
4487 | }
4488 |
4489 | /**
4490 | * Shelves parsed URLs.
4491 | *
4492 | * Stores away a URL fragments that have been parsed
4493 | * and requires no more processing.
4494 | *
4495 | * @param string $text The URL
4496 | * @param string $type The type
4497 | * @return string The fragment's unique reference ID
4498 | * @see Parser::retrieveURLs()
4499 | */
4500 | protected function shelveURL($text, $type = null)
4501 | {
4502 | if ('' === $text) {
4503 | return '';
4504 | }
4505 |
4506 | if ($type === null) {
4507 | $type = 'url';
4508 | }
4509 |
4510 | $this->refCache[$this->refIndex] = $text;
4511 | return $this->uid.($this->refIndex++).':'.$type;
4512 | }
4513 |
4514 | /**
4515 | * Replaces reference tokens with corresponding shelved URL.
4516 | *
4517 | * This method puts all shelved URLs back to the final,
4518 | * parsed input.
4519 | *
4520 | * @param string $text The input
4521 | * @return string Processed text
4522 | * @see Parser::shelveURL()
4523 | */
4524 | protected function retrieveURLs($text)
4525 | {
4526 | return (string)preg_replace_callback(
4527 | '/'.$this->uid.'(?P[0-9]+):(?Purl|image)/',
4528 | array($this, 'retrieveURL'),
4529 | $text
4530 | );
4531 | }
4532 |
4533 | /**
4534 | * Retrieves an URL from the shelve.
4535 | *
4536 | * @param array $m Options
4537 | * @return string The URL
4538 | */
4539 | protected function retrieveURL($m)
4540 | {
4541 | if (!isset($this->refCache[$m['token']])) {
4542 | return '';
4543 | }
4544 |
4545 | $url = $this->refCache[$m['token']];
4546 |
4547 | if (isset($this->urlrefs[$url])) {
4548 | $url = $this->urlrefs[$url];
4549 | }
4550 |
4551 | return $this->rEncodeHTML($this->relURL($url, $m['type']));
4552 | }
4553 |
4554 | /**
4555 | * Whether the URL is valid.
4556 | *
4557 | * Checks are done according the used preferences to
4558 | * determinate whether the URL should be accepted and
4559 | * essentially whether its safe.
4560 | *
4561 | * @param string $url The URL to check
4562 | * @return bool TRUE if valid, FALSE otherwise
4563 | * @since 3.6.0
4564 | */
4565 | protected function isValidUrl($url)
4566 | {
4567 | if ($this->parseURI($url, $component)) {
4568 | if (!isset($component['scheme']) || $component['scheme'] === '') {
4569 | return true;
4570 | }
4571 |
4572 | if (in_array($component['scheme'], $this->url_schemes, true)) {
4573 | return true;
4574 | }
4575 | }
4576 |
4577 | return false;
4578 | }
4579 |
4580 | /**
4581 | * Completes and formats a relative URL.
4582 | *
4583 | * This method adds $this->relativeImagePrefix to the
4584 | * URL if it is relative.
4585 | *
4586 | * The URI is kept as is if it starts with a '/', './', '../',
4587 | * or the URL starts with one of $this->url_schemes. Otherwise
4588 | * the URL is prefixed.
4589 | *
4590 | * @param string $url The URL
4591 | * @param string $type The type
4592 | * @return string Absolute URL
4593 | */
4594 | protected function relURL($url, $type = null)
4595 | {
4596 | if ($this->relativeImagePrefix !== null) {
4597 | // Use legacy fallback if set. Deprecated in 3.7.0.
4598 | $prefix = $this->relativeImagePrefix;
4599 | } elseif ($type === null || $type === 'image') {
4600 | $prefix = $this->relImagePrefix;
4601 | } else {
4602 | $prefix = $this->relLinkPrefix;
4603 | }
4604 |
4605 | if ($prefix) {
4606 | if (strpos($url, '/') === 0 || strpos($url, './') === 0 || strpos($url, '../') === 0 ||
4607 | strpos($url, '#') === 0
4608 | ) {
4609 | return $url;
4610 | }
4611 |
4612 | foreach ($this->url_schemes as $scheme) {
4613 | if (strpos($url, $scheme . ':') === 0) {
4614 | return $url;
4615 | }
4616 | }
4617 |
4618 | return $prefix.$url;
4619 | }
4620 |
4621 | return $url;
4622 | }
4623 |
4624 | /**
4625 | * Checks if an URL is relative.
4626 | *
4627 | * The given URL is considered relative if it
4628 | * start anything other than with '//' or a
4629 | * valid scheme.
4630 | *
4631 | * @param string $url The URL
4632 | * @return bool TRUE if relative, FALSE otherwise
4633 | */
4634 | protected function isRelURL($url)
4635 | {
4636 | if (strpos($url, '//') === 0) {
4637 | return false;
4638 | }
4639 |
4640 | foreach ($this->url_schemes as $scheme) {
4641 | if (strpos($url, $scheme . '://') === 0) {
4642 | return false;
4643 | }
4644 | }
4645 |
4646 | return true;
4647 | }
4648 |
4649 | /**
4650 | * Parses and shelves images in the given input.
4651 | *
4652 | * This method parses the input Textile document for images and
4653 | * generates img HTML tags for each one found, caching the
4654 | * generated img tag internally and replacing the Textile image with a
4655 | * token to the cached tag.
4656 | *
4657 | * @param string $text Textile input
4658 | * @return string The input document with images pulled out and replaced with tokens
4659 | */
4660 | protected function images($text)
4661 | {
4662 | return (string)preg_replace_callback(
4663 | '/
4664 | (?:[[{])? # pre
4665 | \! # opening !
4666 | (?P\<|\=|\>|<|>)? # optional alignment
4667 | (?P'.$this->cls.') # optional attributes
4668 | (?:\.\s)? # optional dot-space
4669 | (?P[^\s(!]+) # presume this is the src
4670 | \s? # optional space
4671 | (?:\((?P[^\)]+)\))? # optional title
4672 | \! # closing
4673 | (?::(?P\S+)(?regex_snippets['mod'],
4676 | array($this, "fImage"),
4677 | $text
4678 | );
4679 | }
4680 |
4681 | /**
4682 | * Checks that the given path is under the document root.
4683 | *
4684 | * @param string $path Path to check
4685 | * @return bool TRUE if path is within the image document root
4686 | * @see Parser::images()
4687 | * @since 3.6.0
4688 | */
4689 | protected function isInDocumentRootDirectory($path)
4690 | {
4691 | $realpath = realpath($path);
4692 |
4693 | if ($realpath) {
4694 | $root = str_replace('\\', '/', $this->getDocumentRootDirectory());
4695 | $realpath = str_replace('\\', '/', $realpath);
4696 | return (0 === strpos($realpath, $root));
4697 | }
4698 |
4699 | return false;
4700 | }
4701 |
4702 | /**
4703 | * Formats an image and stores it on the shelf.
4704 | *
4705 | * @param array $m Options
4706 | * @return string Reference token for the shelved content
4707 | * @see Parser::images()
4708 | */
4709 | protected function fImage($m)
4710 | {
4711 | if (!$this->isValidUrl($m['url'])) {
4712 | return $m[0];
4713 | }
4714 |
4715 | $extras = '';
4716 | $align = (isset($m['align'])) ? $m['align'] : '';
4717 | $atts = $m['atts'];
4718 | $url = $m['url'];
4719 | $title = (isset($m['title'])) ? $m['title'] : '';
4720 | $href = (isset($m['href'])) ? $m['href'] : '';
4721 |
4722 | if ($href && !$this->isValidUrl($href)) {
4723 | return $m[0];
4724 | }
4725 |
4726 | $alignments = array(
4727 | '<' => 'left',
4728 | '=' => 'center',
4729 | '>' => 'right',
4730 | '<' => 'left',
4731 | '>' => 'right',
4732 | );
4733 |
4734 | if (isset($alignments[$align])) {
4735 | if ($this->isAlignClassesEnabled()) {
4736 | $extras = 'align-'.$alignments[$align];
4737 | $align = '';
4738 | } else {
4739 | $align = $alignments[$align];
4740 | }
4741 | } else {
4742 | $align = '';
4743 | }
4744 |
4745 | if ($title) {
4746 | $title = $this->encodeHTML($title);
4747 | }
4748 |
4749 | $img = $this->newTag('img', $this->parseAttribsToArray($atts, '', true, $extras))
4750 | ->align($align)
4751 | ->alt($title, true)
4752 | ->src($this->shelveURL($url, 'image'), true)
4753 | ->title($title);
4754 |
4755 | if (!$this->dimensionless_images && $this->isRelUrl($url)) {
4756 | $location = $this->getDocumentRootDirectory().ltrim($url, '\\/');
4757 | $location_ok = $this->isInDocumentRootDirectory($location);
4758 | if ($location_ok) {
4759 | $real_location = realpath($location);
4760 | if ($real_location && ($size = getimagesize($real_location))) {
4761 | $img->height($size[1])->width($size[0]);
4762 | }
4763 | }
4764 | }
4765 |
4766 | $out = (string) $img;
4767 |
4768 | if ($href) {
4769 | $href = $this->shelveURL($href);
4770 | $link = $this->newTag('a', array(), false)->href($href)->rel($this->rel);
4771 | $out = (string) $link . "$img";
4772 | }
4773 |
4774 | return $this->shelve($out);
4775 | }
4776 |
4777 | /**
4778 | * Parses code blocks in the given input.
4779 | *
4780 | * @param string $text The input
4781 | * @return string Processed text
4782 | */
4783 | protected function code($text)
4784 | {
4785 | $text = $this->doSpecial($text, '', ' ', 'fCode');
4786 | $text = $this->doSpecial($text, '@', '@', 'fCode');
4787 | $text = $this->doSpecial($text, '', ' ', 'fPre');
4788 | return $text;
4789 | }
4790 |
4791 | /**
4792 | * Formats inline code tags.
4793 | *
4794 | * @param array $m
4795 | * @return string
4796 | */
4797 | protected function fCode($m)
4798 | {
4799 | $m = $this->getSpecialOptions($m);
4800 |
4801 | return $m['before'].$this->shelve(''.$this->rEncodeHTML($m['content']).' ').$m['after'];
4802 | }
4803 |
4804 | /**
4805 | * Formats pre tags.
4806 | *
4807 | * @param array $m Options
4808 | * @return string
4809 | */
4810 | protected function fPre($m)
4811 | {
4812 | $m = $this->getSpecialOptions($m);
4813 |
4814 | return $m['before'].''.$this->shelve($this->rEncodeHTML($m['content'])).' '.$m['after'];
4815 | }
4816 |
4817 | /**
4818 | * Shelves parsed content.
4819 | *
4820 | * Stores away a fragment of the source text that have been parsed
4821 | * and requires no more processing.
4822 | *
4823 | * @param string $val The content
4824 | * @return string The fragment's unique reference ID
4825 | * @see Parser::retrieve()
4826 | */
4827 | protected function shelve($val)
4828 | {
4829 | $i = $this->uid.($this->refIndex++).':shelve';
4830 | $this->shelf[$i] = $val;
4831 | return $i;
4832 | }
4833 |
4834 | /**
4835 | * Replaces reference tokens with corresponding shelved content.
4836 | *
4837 | * This method puts all shelved content back to the final,
4838 | * parsed input.
4839 | *
4840 | * @param string $text The input
4841 | * @return string Processed text
4842 | * @see Parser::shelve()
4843 | */
4844 | protected function retrieve($text)
4845 | {
4846 | if ($this->shelf) {
4847 | do {
4848 | $old = $text;
4849 | $text = str_replace(array_keys($this->shelf), $this->shelf, $text);
4850 | } while ($text != $old);
4851 | }
4852 |
4853 | return $text;
4854 | }
4855 |
4856 | /**
4857 | * Removes BOM and unifies line ending in the given input.
4858 | *
4859 | * @param string $text Input Textile
4860 | * @return string Cleaned version of the input
4861 | */
4862 | protected function cleanWhiteSpace($text)
4863 | {
4864 | // Removes byte order mark.
4865 | $out = (string)preg_replace("/^\xEF\xBB\xBF|\x1A/", '', $text);
4866 | // Replaces CRLF and CR with single LF.
4867 | $out = (string)preg_replace("/\r\n?/", "\n", $out);
4868 | // Removes leading tabs and spaces, if the line is otherwise empty.
4869 | $out = (string)preg_replace("/^[ \t]*\n/m", "\n", $out);
4870 | // Removes leading and ending blank lines.
4871 | $out = trim($out, "\n");
4872 | return $out;
4873 | }
4874 |
4875 | /**
4876 | * Removes any unique tokens from the input.
4877 | *
4878 | * @param string $text The input to clean
4879 | * @return string Cleaned input
4880 | * @since 3.5.5
4881 | */
4882 | protected function cleanUniqueTokens($text)
4883 | {
4884 | return str_replace($this->uid, '', $text);
4885 | }
4886 |
4887 | /**
4888 | * Uses the specified callback method to format the content between end and start nodes.
4889 | *
4890 | * @param string $text The input to format
4891 | * @param string $start The start node to look for
4892 | * @param string $end The end node to look for
4893 | * @param string $method The callback method
4894 | * @return string Processed input
4895 | */
4896 | protected function doSpecial($text, $start, $end, $method)
4897 | {
4898 | return (string)preg_replace_callback(
4899 | '/(?P^|\s|[|[({>])'.
4900 | preg_quote($start, '/').'(?P.*?)'.preg_quote($end, '/').
4901 | '(?\]?)/ms',
4902 | array($this, $method),
4903 | $text
4904 | );
4905 | }
4906 |
4907 | /**
4908 | * Gets an array of processed special options.
4909 | *
4910 | * @param array $m Options
4911 | * @return array
4912 | * @since 3.7.2
4913 | */
4914 | protected function getSpecialOptions($m)
4915 | {
4916 | foreach ($this->spanWrappers as $before => $after) {
4917 | if ($m['before'] === $before && $m['after'] === $after) {
4918 | $m['before'] = '';
4919 | $m['after'] = '';
4920 | break;
4921 | }
4922 | }
4923 |
4924 | return $m;
4925 | }
4926 |
4927 | /**
4928 | * Parses notextile tags in the given input.
4929 | *
4930 | * @param string $text The input
4931 | * @return string Processed input
4932 | */
4933 | protected function noTextile($text)
4934 | {
4935 | $text = $this->doSpecial($text, '', ' ', 'fTextile');
4936 | return $this->doSpecial($text, '==', '==', 'fTextile');
4937 | }
4938 |
4939 | /**
4940 | * Format notextile blocks.
4941 | *
4942 | * @param array $m Options
4943 | * @return string
4944 | */
4945 | protected function fTextile($m)
4946 | {
4947 | $m = $this->getSpecialOptions($m);
4948 |
4949 | return $m['before'].$this->shelve($m['content']).$m['after'];
4950 | }
4951 |
4952 | /**
4953 | * Parses footnote reference links in the given input.
4954 | *
4955 | * This method replaces [n] instances with links.
4956 | *
4957 | * @param string $text The input
4958 | * @return string $text Processed input
4959 | * @see Parser::footnoteID()
4960 | */
4961 | protected function footnoteRefs($text)
4962 | {
4963 | return (string)preg_replace_callback(
4964 | '/(?<=\S)\[(?P'.$this->regex_snippets['digit'].'+)'.
4965 | '(?P!?)\]'.$this->regex_snippets['space'].'?/U'.$this->regex_snippets['mod'],
4966 | array($this, 'footnoteID'),
4967 | $text
4968 | );
4969 | }
4970 |
4971 | /**
4972 | * Renders a footnote reference link or ID.
4973 | *
4974 | * @param array $m Options
4975 | * @return string Footnote link, or ID
4976 | */
4977 | protected function footnoteID($m)
4978 | {
4979 | $backref = ' class="footnote"';
4980 |
4981 | if (empty($this->fn[$m['id']])) {
4982 | $this->fn[$m['id']] = $id = $this->linkPrefix . ($this->linkIndex++);
4983 | $backref .= " id=\"fnrev$id\"";
4984 | }
4985 |
4986 | $fnid = $this->fn[$m['id']];
4987 | $footref = ('!' == $m['nolink']) ? $m['id'] : ''.$m['id'].' ';
4988 | $footref = $this->formatFootnote($footref, $backref, false);
4989 |
4990 | return $footref;
4991 | }
4992 |
4993 | /**
4994 | * Parses and shelves quoted quotes in the given input.
4995 | *
4996 | * @param string $text The text to search for quoted quotes
4997 | * @param string $find Pattern to search
4998 | * @return string
4999 | * @since 3.5.4
5000 | */
5001 | protected function glyphQuotedQuote($text, $find = '"?|"[^"]+"')
5002 | {
5003 | return (string)preg_replace_callback(
5004 | "/ (?P{$this->quote_starts})(?P$find)(?P.) /".$this->regex_snippets['mod'],
5005 | array($this, "fGlyphQuotedQuote"),
5006 | $text
5007 | );
5008 | }
5009 |
5010 | /**
5011 | * Formats quoted quotes and stores it on the shelf.
5012 | *
5013 | * @param array $m Named regular expression parts
5014 | * @return string Input with quoted quotes removed and replaced with tokens
5015 | * @see Parser::glyphQuotedQuote()
5016 | */
5017 | protected function fGlyphQuotedQuote($m)
5018 | {
5019 | // Check the correct closing character was found.
5020 | if (!isset($this->quotes[$m['pre']]) || $m['post'] !== $this->quotes[$m['pre']]) {
5021 | return $m[0];
5022 | }
5023 |
5024 | $pre = strtr($m['pre'], array(
5025 | '"' => '“',
5026 | "'" => '‘',
5027 | ' ' => ' ',
5028 | ));
5029 |
5030 | $post = strtr($m['post'], array(
5031 | '"' => '”',
5032 | "'" => '’',
5033 | ' ' => ' ',
5034 | ));
5035 |
5036 | $found = $m['quoted'];
5037 |
5038 | if (strlen($found) > 1) {
5039 | $found = rtrim($this->glyphs($m['quoted']));
5040 | } elseif ('"' === $found) {
5041 | $found = """;
5042 | }
5043 |
5044 | return $this->shelve(' '.$pre.$found.$post.' ');
5045 | }
5046 |
5047 | /**
5048 | * Replaces glyphs in the given input.
5049 | *
5050 | * This method performs typographical glyph replacements. The input is split
5051 | * across HTML-like tags in order to avoid attempting glyph
5052 | * replacements within tags.
5053 | *
5054 | * @param string $text Input Textile
5055 | * @return string
5056 | */
5057 | protected function glyphs($text)
5058 | {
5059 | if (!$this->glyph_search) {
5060 | return $text;
5061 | }
5062 |
5063 | // Fix: hackish -- adds a space if final char of text is a double quote.
5064 | if (($text = preg_replace('/"\z/', "\" ", $text)) === null) {
5065 | return '';
5066 | }
5067 |
5068 | $text = preg_split(
5069 | "@(<[\w/!?].*>)@Us".$this->regex_snippets['mod'],
5070 | $text,
5071 | -1,
5072 | PREG_SPLIT_DELIM_CAPTURE
5073 | );
5074 |
5075 | if ($text === false) {
5076 | return '';
5077 | }
5078 |
5079 | $i = 0;
5080 | $glyph_out = array();
5081 |
5082 | foreach ($text as $line) {
5083 | // Text tag text tag text ...
5084 | if (++$i % 2) {
5085 | // Raw < > & chars are already entity encoded in restricted mode
5086 | if (!$this->isRestrictedModeEnabled()) {
5087 | $line = preg_replace('/&(?!#?[a-z0-9]+;)/i', '&', $line);
5088 | $line = str_replace(array('<', '>'), array('<', '>'), (string)$line);
5089 | }
5090 |
5091 | $line = preg_replace($this->glyph_search, $this->glyph_replace, $line);
5092 | }
5093 |
5094 | $glyph_out[] = $line;
5095 | }
5096 |
5097 | return join('', $glyph_out);
5098 | }
5099 |
5100 | /**
5101 | * Replaces glyph references in the given input.
5102 | *
5103 | * This method removes temporary glyph: instances
5104 | * from the input.
5105 | *
5106 | * @param string $text The input
5107 | * @return string Processed input
5108 | */
5109 | protected function replaceGlyphs($text)
5110 | {
5111 | return str_replace($this->uid.':glyph:', '', $text);
5112 | }
5113 |
5114 | /**
5115 | * Translates alignment tag into corresponding CSS text-align property value.
5116 | *
5117 | * @param string $in The Textile alignment tag
5118 | * @return string CSS text-align value
5119 | */
5120 | protected function hAlign($in)
5121 | {
5122 | $vals = array(
5123 | '<' => 'left',
5124 | '>' => 'right',
5125 | '<>' => 'justify',
5126 | '<' => 'left',
5127 | '=' => 'center',
5128 | '>' => 'right',
5129 | '<>' => 'justify',
5130 | );
5131 |
5132 | return (isset($vals[$in])) ? $vals[$in] : '';
5133 | }
5134 |
5135 | /**
5136 | * Translates vertical alignment tag into corresponding CSS vertical-align property value.
5137 | *
5138 | * @param string $in The Textile alignment tag
5139 | * @return string CSS vertical-align value
5140 | */
5141 | protected function vAlign($in)
5142 | {
5143 | $vals = array(
5144 | '^' => 'top',
5145 | '-' => 'middle',
5146 | '~' => 'bottom',
5147 | );
5148 |
5149 | return (isset($vals[$in])) ? $vals[$in] : '';
5150 | }
5151 |
5152 | /**
5153 | * Converts character codes in the given input from HTML numeric character reference to character code.
5154 | *
5155 | * Conversion is done according to Textile's multi-byte conversion map.
5156 | *
5157 | * @param string $text The input
5158 | * @param string $charset The character set
5159 | * @return string Processed input
5160 | */
5161 | protected function encodeHigh($text, $charset = 'UTF-8')
5162 | {
5163 | if ($this->isMultiByteStringSupported()) {
5164 | return mb_encode_numericentity($text, $this->cmap, $charset);
5165 | }
5166 |
5167 | return htmlentities($text, ENT_NOQUOTES, $charset);
5168 | }
5169 |
5170 | /**
5171 | * Converts numeric HTML character references to character code.
5172 | *
5173 | * @param string $text The input
5174 | * @param string $charset The character set
5175 | * @return string Processed input
5176 | */
5177 | protected function decodeHigh($text, $charset = 'UTF-8')
5178 | {
5179 | $text = (string) intval($text) === (string) $text ? "$text;" : "&$text;";
5180 |
5181 | if ($this->isMultiByteStringSupported()) {
5182 | return mb_decode_numericentity($text, $this->cmap, $charset);
5183 | }
5184 |
5185 | return html_entity_decode($text, ENT_NOQUOTES, $charset);
5186 | }
5187 |
5188 | /**
5189 | * Convert special characters to HTML entities.
5190 | *
5191 | * This method's functionality is identical to PHP's own
5192 | * htmlspecialchars(). In Textile this is used for sanitising
5193 | * the input.
5194 | *
5195 | * @param string $str The string to encode
5196 | * @param bool $quotes Encode quotes
5197 | * @return string Encoded string
5198 | * @see htmlspecialchars()
5199 | */
5200 | protected function encodeHTML($str, $quotes = true)
5201 | {
5202 | $a = array(
5203 | '&' => '&',
5204 | '<' => '<',
5205 | '>' => '>',
5206 | );
5207 |
5208 | if ($quotes) {
5209 | $a = $a + array(
5210 | "'" => ''', // Numeric, as in htmlspecialchars
5211 | '"' => '"',
5212 | );
5213 | }
5214 |
5215 | return str_replace(array_keys($a), $a, $str);
5216 | }
5217 |
5218 | /**
5219 | * Convert special characters to HTML entities.
5220 | *
5221 | * This is identical to encodeHTML(), but takes restricted
5222 | * mode into account. When in restricted mode, only escapes
5223 | * quotes.
5224 | *
5225 | * @param string $str The string to encode
5226 | * @param bool $quotes Encode quotes
5227 | * @return string Encoded string
5228 | * @see Parser::encodeHTML()
5229 | */
5230 | protected function rEncodeHTML($str, $quotes = true)
5231 | {
5232 | // In restricted mode, all input but quotes has already been escaped
5233 | if ($this->isRestrictedModeEnabled()) {
5234 | return str_replace('"', '"', $str);
5235 | }
5236 |
5237 | return $this->encodeHTML($str, $quotes);
5238 | }
5239 |
5240 | /**
5241 | * Whether multiple mbstring extensions is loaded.
5242 | *
5243 | * @return bool
5244 | * @since 3.5.5
5245 | */
5246 | protected function isMultiByteStringSupported()
5247 | {
5248 | if ($this->mb === null) {
5249 | $this->mb = is_callable('mb_strlen');
5250 | }
5251 |
5252 | return $this->mb;
5253 | }
5254 |
5255 | /**
5256 | * Whether PCRE supports UTF-8.
5257 | *
5258 | * @return bool
5259 | * @since 3.5.5
5260 | */
5261 | protected function isUnicodePcreSupported()
5262 | {
5263 | return (bool) @preg_match('/\pL/u', 'a');
5264 | }
5265 | }
5266 |
--------------------------------------------------------------------------------
/src/Netcarver/Textile/Tag.php:
--------------------------------------------------------------------------------
1 | class('big blue')->src('images/elephant.jpg');
52 | *
53 | * @method Tag alt(string $text, bool $allowEmpty = false)
54 | * @method Tag align(string $alignment)
55 | * @method Tag height(string|int $height)
56 | * @method Tag href(string $url, bool $allowEmpty = false)
57 | * @method Tag rel(string $relationship)
58 | * @method Tag src(string $url, bool $allowEmpty = false)
59 | * @method Tag title(string $title)
60 | * @method Tag width(string|int $width)
61 | * @internal
62 | */
63 | class Tag extends DataBag
64 | {
65 | /**
66 | * The name of the tag.
67 | *
68 | * @var string|null
69 | */
70 | protected $tag;
71 |
72 | /**
73 | * Whether the tag is self-closing.
74 | *
75 | * @var bool
76 | */
77 | protected $selfclose;
78 |
79 | /**
80 | * Constructor.
81 | *
82 | * @param string|null $name The tag name
83 | * @param array $attributes An array of attributes
84 | * @param bool $selfclosing Whether the tag is self-closing
85 | */
86 | public function __construct($name, $attributes = null, $selfclosing = true)
87 | {
88 | parent::__construct($attributes);
89 | $this->tag = $name;
90 | $this->selfclose = $selfclosing;
91 | }
92 |
93 | /**
94 | * Returns the tag as HTML.
95 | *
96 | * bc. $img = new Tag('img');
97 | * $img->src('images/example.jpg')->alt('Example image');
98 | * echo (string) $img;
99 | *
100 | * @return string A HTML element
101 | */
102 | public function __toString()
103 | {
104 | $attributes = '';
105 |
106 | if ($this->data) {
107 | ksort($this->data);
108 | foreach ($this->data as $name => $value) {
109 | $attributes .= " $name=\"$value\"";
110 | }
111 | }
112 |
113 | if ($this->tag) {
114 | return '<' . $this->tag . $attributes . (($this->selfclose) ? ' />' : '>');
115 | }
116 |
117 | return $attributes;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
| |