cgi-bin/tek/class/markup.lua
author Timm S. Mueller <tmueller@neoscientists.org>
Sat, 15 Dec 2007 17:32:40 +0100
changeset 223 b2151b233fa7
parent 220 b2e0af2b5a8f
child 228 4386f52e29fb
permissions -rw-r--r--
Incomplete Markup class instantiation; indentation now using <div> again
     1 -------------------------------------------------------------------------------
     2 --
     3 --	tek.class.markup
     4 --	Written by Timm S. Mueller <tmueller@schulze-mueller.de>
     5 --	See copyright notice in COPYRIGHT
     6 --
     7 --	OVERVIEW::
     8 --	Markup parser - produces fancy HTML from plain text with special
     9 --	formattings.
    10 --
    11 -------------------------------------------------------------------------------
    12 
    13 local Class = require "tek.class"
    14 
    15 local type = type
    16 local insert = table.insert
    17 local remove = table.remove
    18 local concat = table.concat
    19 local min = math.min
    20 local max = math.max
    21 local char = string.char
    22 local stdin = io.stdin
    23 local stdout = io.stdout
    24 local ipairs = ipairs
    25 
    26 module("tek.class.markup", tek.class)
    27 
    28 _VERSION = "Markup 1.0"
    29 local Markup = _M
    30 
    31 -------------------------------------------------------------------------------
    32 --	iterate over lines in a string
    33 -------------------------------------------------------------------------------
    34 
    35 local function rd_string(s)
    36 	local pos = 1
    37 	return function()
    38 		if pos then
    39 			local a, e = s:find("[\r]?\n", pos)
    40 			local o = pos
    41 			if not a then
    42 				pos = nil
    43 				return s:sub(o)
    44 			else
    45 				pos = e + 1
    46 				return s:sub(o, a - 1)
    47 			end
    48 		end
    49 	end
    50 end
    51 
    52 -------------------------------------------------------------------------------
    53 --	utf8values: iterator over UTF-8 encoded Unicode chracter codes
    54 -------------------------------------------------------------------------------
    55 
    56 local function utf8values(s)
    57 
    58 	local readc
    59 	local i = 0
    60 	if type(s) == "string" then
    61 		readc = function()
    62 			i = i + 1
    63 			return s:byte(i)
    64 		end
    65 	else
    66 		readc = function()
    67 			local c = s:read(1)
    68 			return c and c:byte(1)
    69 		end
    70 	end
    71 
    72 	local accu = 0
    73 	local numa = 0
    74 	local min, buf
    75 
    76 	return function()
    77 		local c
    78 		while true do
    79 			if buf then
    80 				c = buf
    81 				buf = nil
    82 			else
    83 				c = readc()
    84 			end
    85 			if not c then
    86 				return
    87 			end
    88 			if c == 254 or c == 255 then
    89 				break
    90 			end
    91 			if c < 128 then
    92 				if numa > 0 then
    93 					buf = c
    94 					break
    95 				end
    96 				return c
    97 			elseif c < 192 then
    98 				if numa == 0 then break end
    99 				accu = accu * 64 + c - 128
   100 				numa = numa - 1
   101 				if numa == 0 then
   102 					if accu == 0 or accu < min or
   103 						(accu >= 55296 and accu <= 57343) then
   104 						break
   105 					end
   106 					c = accu
   107 					accu = 0
   108 					return c
   109 				end
   110 			else
   111 				if numa > 0 then
   112 					buf = c
   113 					break
   114 				end
   115 				if c < 224 then
   116 					min = 128
   117 					accu = c - 192
   118 					numa = 1
   119 				elseif c < 240 then
   120 					min = 2048
   121 					accu = c - 224
   122 					numa = 2
   123 				elseif c < 248 then
   124 					min = 65536
   125 					accu = c - 240
   126 					numa = 3
   127 				elseif c < 252 then
   128 					min = 2097152
   129 					accu = c - 248
   130 					numa = 4
   131 				else
   132 					min = 67108864
   133 					accu = c - 252
   134 					numa = 5
   135 				end
   136 			end
   137 		end
   138 		accu = 0
   139 		numa = 0
   140 		return 65533 -- bad character
   141 	end
   142 end
   143 
   144 -------------------------------------------------------------------------------
   145 --	encodeform: encode for forms (display '<', '>', '&', '"' literally)
   146 -------------------------------------------------------------------------------
   147 
   148 function Markup:encodeform(s)
   149 	local tab = { }
   150 	if s then
   151 		for c in utf8values(s) do
   152 			if c == 34 then
   153 				insert(tab, "&quot;")
   154 			elseif c == 38 then
   155 				insert(tab, "&amp;")
   156 			elseif c == 60 then
   157 				insert(tab, "&lt;")
   158 			elseif c == 62 then
   159 				insert(tab, "&gt;")
   160 			elseif c == 91 or c == 93 or c > 126 then
   161 				insert(tab, ("&#%03d;"):format(c))
   162 			else
   163 				insert(tab, char(c))
   164 			end
   165 		end
   166 	end
   167 	return concat(tab)
   168 end
   169 
   170 -------------------------------------------------------------------------------
   171 --	encodeurl: encode string to url; optionally specify a string with a
   172 --	set of characters that should be left unmodified, from: $&+,/:;=?@
   173 -------------------------------------------------------------------------------
   174 
   175 local function encodefunc(c)
   176 	return ("%%%02x"):format(c:byte())
   177 end
   178 
   179 function Markup:encodeurl(s, excl)
   180 	-- reserved chars with special meaning:
   181 	local matchset = "$&+,/:;=?@"
   182 	if excl == true then
   183 		matchset = ""
   184 	elseif excl and type(excl) == "string" then
   185 		matchset = matchset:gsub("[" .. excl:gsub(".", "%%%1") .. "]", "")
   186 	end
   187 	-- unsafe chars are always substituted:
   188 	matchset = matchset .. '"<>#%{}|\\^~[]`]'
   189 	matchset = "[%z\001-\032\127-\255" .. matchset:gsub(".", "%%%1") .. "]"
   190 	return s:gsub(matchset, encodefunc)
   191 end
   192 
   193 -------------------------------------------------------------------------------
   194 --	get a SGML/XML tag
   195 -------------------------------------------------------------------------------
   196 
   197 function Markup:getTagML(id, tag, open)
   198 	if tag then
   199 		local tab = { tag }
   200 		if id == "link" or id == "emphasis" or id == "code" then
   201 			if not self.brpend and not self.inline then
   202 				insert(tab, 1, ("\t"):rep(self.depth))
   203 			end
   204 			self.inline = true
   205 			self.brpend = nil
   206 		else
   207 			if id == "image" then
   208 				if not self.inline then
   209 					insert(tab, 1, ("\t"):rep(self.depth))
   210 				end
   211 				self.brpend = true
   212 			else
   213 				if id == "pre" or id == "preline" then
   214 					self.brpend = nil
   215 				elseif open or id ~= "preline" then
   216 					insert(tab, 1, ("\t"):rep(self.depth))
   217 					if not open and self.inline then
   218 						self.brpend = true
   219 					end
   220 				end
   221 				if self.brpend then
   222 					insert(tab, 1, "\n")
   223 				end
   224 				if id ~= "preline" or not open then
   225 					insert(tab, "\n")
   226 				end
   227 				self.inline = nil
   228 				self.brpend = nil
   229 			end
   230 		end
   231 		return concat(tab)
   232 	end
   233 end
   234 
   235 -------------------------------------------------------------------------------
   236 --	get a SGML/XML line of text
   237 -------------------------------------------------------------------------------
   238 
   239 function Markup:getTextML(line, id)
   240 	local tab = { }
   241 	if self.brpend then
   242 		insert(tab, "\n")
   243 		insert(tab, ("\t"):rep(self.depth))
   244 	else
   245 		if id ~= "link" and id ~= "emphasis" and id ~= "code" then
   246 			if not self.inline then
   247 				insert(tab, ("\t"):rep(self.depth))
   248 			end
   249 		end
   250 	end
   251 	line:gsub("^(%s*)(.-)(%s*)$", function(a, b, c)
   252 		if a ~= "" then
   253 			insert(tab, " ")
   254 		end
   255 		insert(tab, b)
   256 		if c ~= "" then
   257 			insert(tab, " ")
   258 		end
   259 	end)
   260 	self.brpend = true
   261 	return concat(tab)
   262 end
   263 
   264 -------------------------------------------------------------------------------
   265 --	definitions for output as XHTML 1.0 strict
   266 -------------------------------------------------------------------------------
   267 
   268 function Markup:out(...)
   269 	self.wrfunc(concat { ... })
   270 end
   271 
   272 function Markup:init(docname)
   273 	self.depth = 1
   274 	return [[
   275 <?xml version="1.0" encoding="utf-8" ?>
   276 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   277 	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
   278 <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
   279 	<head>
   280 		<title>]] .. docname .. [[</title>
   281 		<link rel="stylesheet" href="manual.css" />
   282 	</head>
   283 	<body>
   284 ]]
   285 end
   286 
   287 function Markup:exit()
   288 	return [[
   289 	</body>
   290 </html>
   291 ]]
   292 end
   293 
   294 function Markup:gettag(id, tag, open)
   295 	return self:getTagML(id, tag, open)
   296 end
   297 
   298 function Markup:gettext(line, id)
   299 	return self:encodeform(self:getTextML(line, id))
   300 end
   301 
   302 function Markup:getpre(line)
   303 	return self:encodeform(line), '\n'
   304 end
   305 
   306 function Markup:getcode(line)
   307 	return self:encodeform(line)
   308 end
   309 
   310 function Markup:head(level, text)
   311 	return '<h' .. level .. '>', '</h' .. level .. '>'
   312 end
   313 
   314 function Markup:node(id, text)
   315 	return ('<div class="node"><h3><a name="%s" id="%s"><code>%s</code></a></h3>'):format(
   316 		id, id, text), '</div>'
   317 end
   318 
   319 function Markup:headnode(id, text, len, is_code)
   320 	if is_code then
   321 		return ('<hr /><div class="node"><h%d><a name="%s" id="%s"><code>%s</code></a></h%d>'):format(
   322 			len, id, id, text, len), '</div>'
   323 	else
   324 		return ('<hr /><div class="node"><h%d><a name="%s" id="%s">%s</a></h%d>'):format(
   325 			len, id, id, text, len), '</div>'
   326 	end
   327 end
   328 
   329 function Markup:def(name)
   330 	self.prebr = nil
   331 	return '<div class="definition"><dfn>' .. name .. '</dfn>', '</div>'
   332 end
   333 
   334 function Markup:indent()
   335 	return '<blockquote>', '</blockquote>'
   336 end
   337 
   338 function Markup:list()
   339 	return '<ul>', '</ul>'
   340 end
   341 
   342 function Markup:item(bullet)
   343 	if bullet then
   344 		return '<li>', '</li>'
   345 	end
   346 	return '<li style="list-style-type: none">', '</li>'
   347 end
   348 
   349 function Markup:block()
   350 	return '<p>', '</p>'
   351 end
   352 
   353 function Markup:rule()
   354 	return '<hr />'
   355 end
   356 
   357 function Markup:pre()
   358 	return '<pre>', '</pre>'
   359 end
   360 
   361 function Markup:code()
   362 	return '<code>', '</code>'
   363 end
   364 
   365 function Markup:emphasis(len, text)
   366 	if len == 1 then
   367 		return '<em>', '</em>'
   368 	elseif len == 2 then
   369 		return '<strong>', '</strong>'
   370 	else
   371 		return '<em><strong>', '</strong></em>'
   372 	end
   373 end
   374 
   375 function Markup:link(link)
   376 	--local isurl = link:match("^%a*://.*$")
   377 	local func = link:match("^(.*)%(%)$")
   378 	if func then
   379 		return '<a href="#' .. func .. '"><code>', '</code></a>'
   380 	else
   381 		return '<a href="' .. link .. '">', '</a>'
   382 	end
   383 end
   384 
   385 function Markup:table(border)
   386 	return '<table>', '</table>'
   387 end
   388 
   389 function Markup:row()
   390 	self.column = 0
   391 	return '<tr>', '</tr>'
   392 end
   393 
   394 function Markup:cell(border)
   395 	self.column = self.column + 1
   396 	return '<td class="column' .. self.column .. '">', '</td>'
   397 end
   398 
   399 function Markup:image(link)
   400 	return '<img src="' .. self:encodeurl(link, true) .. '" />'
   401 end
   402 
   403 -------------------------------------------------------------------------------
   404 --	run: Runs the parser
   405 -------------------------------------------------------------------------------
   406 
   407 function Markup:run()
   408 
   409 	local function doid(id, ...)
   410 		if self[id] then
   411 			return self[id](self, ...)
   412 		end
   413 	end
   414 
   415 	local function doout(id, ...)
   416 		doid("out", doid(id, ...))
   417 	end
   418 
   419 	local function push(id, ...)
   420 		local opentag, closetag = doid(id, ...)
   421 		doout("gettag", id, opentag, true)
   422 		insert(self.stack, { id = id, closetag = closetag })
   423 		self.depth = self.depth + 1
   424 	end
   425 
   426 	local function pop()
   427 		local e = remove(self.stack)
   428 		if e then
   429 			self.depth = self.depth - 1
   430 			doout("gettag", e.id, e.closetag)
   431 			return e.id
   432 		end
   433 	end
   434 
   435 	local function top()
   436 		local e = self.stack[#self.stack]
   437 		if e then
   438 			return e.id
   439 		end
   440 	end
   441 
   442 	local function popuntil(...)
   443 		local i
   444 		repeat
   445 			i = pop()
   446 			for _, v in ipairs { ... } do
   447 				if v == i then
   448 					return
   449 				end
   450 			end
   451 		until not i
   452 	end
   453 
   454 	local function popwhilenot(...)
   455 		local i
   456 		repeat
   457 			i = top()
   458 			for _, v in ipairs { ... } do
   459 				if v == i then
   460 					return
   461 				end
   462 			end
   463 			pop()
   464 		until not i
   465 	end
   466 
   467 	local function popwhile(...)
   468 		local cont
   469 		repeat
   470 			local id = top()
   471 			cont = false
   472 			for _, v in ipairs { ... } do
   473 				if v == id then
   474 					local i = pop()
   475 					cont = true
   476 					break
   477 				end
   478 			end
   479 		until not cont
   480 	end
   481 
   482 	local function popif(...)
   483 		local i = top()
   484 		for _, v in ipairs { ... } do
   485 			if v == i then
   486 				pop()
   487 				return
   488 			end
   489 		end
   490 	end
   491 
   492 	local function checktop(...)
   493 		local i = top()
   494 		for _, v in ipairs { ... } do
   495 			if v == i then
   496 				return true
   497 			end
   498 		end
   499 	end
   500 
   501 	local function pushfragment(...)
   502 		local line = concat { ... }
   503 		if not self.context.fragments then
   504 			self.context.fragments = { }
   505 		end
   506 		self.id = (self.id or 0) + 1
   507 		insert(self.context.fragments, 1, { line = line, id = self.id })
   508 		self.context.topid = self.id
   509 	end
   510 
   511 	local function popfragment()
   512 		self.context.firstfragment = nil
   513 		local frag = remove(self.context.fragments, 1)
   514 		if frag then
   515 			self.line = frag.line
   516 			if frag.id == self.context.topid then
   517 				self.context.firstfragment = self.context.parentfirstfragment
   518 			end
   519 		else
   520 			self.line = nil
   521 		end
   522 		return self.line
   523 	end
   524 
   525 	local function pushcontext(id, line)
   526 		insert(self.cstack, self.context)
   527 		if id then
   528 			self.context = { id = id, fragments = { } }
   529 			pushfragment(line)
   530 			insert(self.cstack, self.context)
   531 		end
   532 	end
   533 
   534 	local function popcontext()
   535 		self.context = remove(self.cstack)
   536 		return self.context
   537 	end
   538 
   539 	-- parse
   540 
   541 	self.lnr = 1
   542 	self.previndent = 0
   543 	self.stack = { }
   544 	self.depth = 0
   545 	self.in_table = nil
   546 	self.prebr = nil
   547 	self.is_dynamic_content = nil
   548 
   549 	doout("init", self.docname)
   550 	push("document")
   551 
   552 	local linematch = ("^(%s*)(.-)%%s*$"):format(self.indentchar)
   553 
   554 	local feature = { }
   555 	self.features:gsub("%a", function(f)
   556 		feature[f] = true
   557 	end)
   558 
   559 	for line in self.rdfunc(self.input) do
   560 
   561 		self.indentlevel = 0
   562 		line = line:gsub(linematch, function(s, t)
   563 			if t ~= "" then
   564 				self.indentlevel = s:len()
   565 			else
   566 				self.indentlevel = self.previndent
   567 			end
   568 			return t
   569 		end)
   570 
   571 		if feature["t"] then
   572 			self.istabline = line:find("||", 1, 1)
   573 			if self.istabline then
   574 				self.indentlevel = self.previndent
   575 			end
   576 			if self.in_table then
   577 				popuntil("row")
   578 				if not self.istabline then
   579 					popuntil("table")
   580 					self.in_table = nil
   581 				end
   582 			end
   583 		end
   584 
   585 		if self.indentlevel < self.previndent then
   586 			if not self.preindent or self.indentlevel < self.preindent then
   587 				local i = self.indentlevel
   588 				while i < self.previndent do
   589 					popuntil("indent", "pre")
   590 					i = i + 1
   591 				end
   592 				self.preindent = nil
   593 			end
   594 		elseif self.indentlevel == self.previndent + 1 then
   595 			popif("block")
   596 			push("indent")
   597 		elseif feature["p"] and self.indentlevel >= self.previndent + 2 then
   598 			if not self.preindent then
   599 				self.preindent = self.previndent + 2
   600 				popif("block")
   601 				push("pre")
   602 			end
   603 		end
   604 
   605 		if not self.preindent then
   606 
   607 			if feature["d"] then
   608 				-- def ( SYNOPSIS )
   609 				line = line:gsub("^(%u[%u%d%s_]+)::$", function(name)
   610 					popwhile("def", "item", "list", "block", "pre")
   611 					push("def", name)
   612 					return ""
   613 				end)
   614 			end
   615 
   616 			if feature["f"] then
   617 				-- function ( INCLUDE(name) )
   618 				line = line:gsub("^%s*(INCLUDE_?%a*)%(%s*(.*)%s*%)%s*$",
   619 					function(key, line)
   620 					if key == "INCLUDE" or key == "INCLUDE_STATIC" then
   621 						local args = { }
   622 						line:gsub(",?([^,]*)", function(a)
   623 							a = a:match("^%s*(%S.-)%s*$")
   624 							if a then
   625 								insert(args, a)
   626 							end
   627 						end)
   628 						popwhilenot("document")
   629 						push("func", key == "INCLUDE", args)
   630 						return ""
   631 					end
   632 				end)
   633 				-- argument ( ======== )
   634 				line = line:gsub("^%s*========+%s*$", function()
   635 					popwhilenot("func")
   636 					push("argument")
   637 					return ""
   638 				end)
   639 			end
   640 
   641 -- 			-- node ( @@ ... : ... @@ )
   642 --
   643 -- 			line = line:gsub(
   644 -- 				"^%s*@@%s+(.+)%s+:%s+(.-)%s+@@%s*$", function(text, id)
   645 -- 				popwhilenot("document")
   646 -- 				id = id:gsub("[^a-zA-Z%_%-%.%:]", "")
   647 -- 				push("node", id, text)
   648 -- 				return ""
   649 -- 			end)
   650 --
   651 -- 			-- node ( @@ ... @@ )
   652 --
   653 -- 			line = line:gsub("^%s*@@%s+(.+)%s+@@%s*$", function(text)
   654 -- 				popwhilenot("document")
   655 -- 				local id = text:gsub("[^a-zA-Z%_%-%.%:]", "")
   656 -- 				push("node", id, text)
   657 -- 				return ""
   658 -- 			end)
   659 
   660 			if feature["n"] then
   661 				-- headnode ( ==( id : text )== )
   662 				line = line:gsub("^%s*(=+[%(%[])%s+(.+)%s+:%s+(.+)%s+([%)%]]=+)%s*$",
   663 					function(s1, id, text, s2)
   664 					local l = min(s1:len() - 1, s2:len() - 1, 5)
   665 					popwhilenot("document")
   666 					local id = id:gsub("[^a-zA-Z%_%-%.%:]", "")
   667 					push("headnode", id, text, l, s2:sub(1, 1) == "]")
   668 					return ""
   669 				end)
   670 				-- headnode ( ==( text )== )
   671 				line = line:gsub("^%s*(=+[%(%[])%s+(.*)%s+([%)%]]=+)%s*$",
   672 					function(s1, text, s2)
   673 					local l = min(s1:len(), s2:len(), 5)
   674 					popwhilenot("document")
   675 					local id = text:gsub("[^a-zA-Z%_%-%.%:]", "")
   676 					push("headnode", id, text, l, s2:sub(1, 1) == "]")
   677 					return ""
   678 				end)
   679 			end
   680 
   681 			if feature["s"] then
   682 				-- rule ( ----.. )
   683 				line = line:gsub("^%s*(%-%-%-%-+)%s*$", function(s)
   684 					popwhile("block", "list", "item")
   685 					push("rule")
   686 					pop()
   687 					return ""
   688 				end)
   689 			end
   690 
   691 		end
   692 
   693 		--
   694 
   695 		self.cstack = { }
   696 		self.context = { parentfirstfragment = true }
   697 		pushfragment(line)
   698 		pushcontext()
   699 
   700 		if line == "" then
   701 			popwhile("block")
   702 		end
   703 
   704 		while popcontext() do
   705 			while popfragment() do
   706 				line = self.line
   707 
   708 				if self.preindent then
   709 
   710 					if self.prepend then
   711 						push("preline")
   712 						doout("getpre", "")
   713 						pop()
   714 						self.prepend = nil
   715 					end
   716 
   717 					if line == "" then
   718 						self.prepend = true
   719 					else
   720 						push("preline")
   721 						doout("getpre", (" "):rep(self.indentlevel -
   722 							self.preindent) .. line)
   723 						pop()
   724 					end
   725 
   726 				else
   727 					self.prepend = nil
   728 
   729 					if line ~= "" then
   730 
   731 						if feature["l"] then
   732 							-- list/item ( * ... )
   733 							local _, _, b, a = line:find("^([%*%-])%s+(.*)$")
   734 							if b and a then
   735 								local inlist = checktop("item", "list")
   736 								popwhile("item", "block")
   737 								if not inlist then
   738 									push("list")
   739 								end
   740 								if b == "*" then
   741 									push("item", true)
   742 								else
   743 									push("item")
   744 								end
   745 								pushcontext("item", a)
   746 								break
   747 							end
   748 						end
   749 
   750 						if feature["t"] then
   751 							-- table cells
   752 							local _, pos, a, b =
   753 								line:find("^%s*(.-)%s*||%s*(.*)%s*$")
   754 							if pos then
   755 								if self.context.id == "table" then
   756 									popuntil("cell")
   757 								else
   758 									self.context.id = "table"
   759 									if not self.in_table then
   760 										self.in_table = true
   761 										push("table")
   762 									end
   763 									push("row")
   764 								end
   765 								pushfragment(b)
   766 								push("cell")
   767 								pushcontext("cell", a)
   768 								break
   769 							elseif self.context.id == "table" then
   770 								popuntil("cell")
   771 								push("cell")
   772 								pushcontext("cell", line)
   773 								break
   774 							end
   775 						end
   776 
   777 						if feature["c"] then
   778 							-- code
   779 							local _, _, a, text, b =
   780 								line:find("^(.-){{(.-)}}(.*)%s*$")
   781 							if text then
   782 								if a == "" then
   783 									if not checktop("block", "item", "cell") then
   784 										push("block")
   785 									end
   786 									if self.context.firstfragment then
   787 										doout("gettext", "")
   788 									end
   789 									push("code")
   790 									if b == "" then b = " " end
   791 									pushfragment(b)
   792 									pushcontext("code", text)
   793 								else
   794 									pushfragment("{{", text, "}}", b)
   795 									pushfragment(a)
   796 									pushcontext()
   797 								end
   798 								break
   799 							end
   800 						end
   801 
   802 						if feature["e"] then
   803 							-- emphasis
   804 							local _, _, a, x, text, y, b =
   805 								line:find("^(.-)(''+)(.-)(''+)(.*)$")
   806 							if text then
   807 								if a == "" then
   808 									x, y = x:len(), y:len()
   809 									local len = min(x, y, 4)
   810 									if not checktop("block", "item", "cell") then
   811 										push("block")
   812 									end
   813 									if self.context.firstfragment then
   814 										doout("gettext", "")
   815 									end
   816 									push("emphasis", len - 1)
   817 									if b == "" then b = " " end
   818 									pushfragment(b)
   819 									pushcontext("emphasis", text)
   820 								else
   821 									pushfragment(x, text, y, b)
   822 									pushfragment(a)
   823 									pushcontext()
   824 								end
   825 								break
   826 							end
   827 						end
   828 
   829 						if feature["l"] then
   830 							-- [[link]], [[title][link]], function(), [[link : title]]
   831 							if self.context.id ~= "link"
   832 								and self.context.id ~= "code" then
   833 								local a, title, link, b
   834 								a, link, title, b = -- [[link : title]]
   835 									line:match("^(.-)%[%[(.-)%s+:%s+(.-)%]%](.*)%s*$")
   836 								if not link then
   837 									a, title, link, b = -- [[text][...]]
   838 										line:match("^(.-)%[%[(.-)%]%[(.-)%]%](.*)%s*$")
   839 								end
   840 								if not link then -- [[....]]
   841 									a, link, b = line:match("^(.-)%[%[(.-)%]%](.*)%s*$")
   842 								end
   843 								if not link then -- class:function()
   844 									a, link, b = line:match("^(.-)(%a[%w_:]-%(%))(.*)%s*$")
   845 								end
   846 								if not link then -- prot://foo/bar
   847 									a, link, b = line:match(
   848 										"^(.-)(%a*://[%w_%-%.,:;/%?=~]*)(.*)%s*$")
   849 								end
   850 								if link then
   851 									if a == "" then
   852 										if not checktop("block", "item", "cell") then
   853 											push("block")
   854 										end
   855 										if self.context.firstfragment then
   856 											doout("gettext", "")
   857 										end
   858 										push("link", link)
   859 										if b == "" then b = " " end
   860 										pushfragment(b)
   861 										pushcontext("link", title or link)
   862 									else
   863 										pushfragment("[[", title or link, "][", link, "]]", b)
   864 										pushfragment(a)
   865 										pushcontext()
   866 									end
   867 									break
   868 								end
   869 							end
   870 						end
   871 
   872 						if feature["i"] then
   873 							-- imglink (@@...@@)
   874 							line = line:gsub("@@(.-)@@", function(link)
   875 								push("image", link)
   876 								pop()
   877 								return ""
   878 							end)
   879 						end
   880 
   881 						if feature["h"] then
   882 							-- head ( = ... = )
   883 							line = line:gsub("(=+)%s+(.*)%s+(=+)",
   884 								function(s1, text, s2)
   885 								local l = min(s1:len(), s2:len(), 5)
   886 								popwhile("block", "item", "list")
   887 								push("head", l)
   888 								return text
   889 							end)
   890 						end
   891 
   892 						-- output
   893 						if line ~= "" then
   894 							if not checktop("item", "block", "cell", "pre",
   895 								"head", "emphasis", "link", "code") then
   896 								popwhile("item", "list", "pre", "code")
   897 								push("block")
   898 							end
   899 							if top() == "code" then
   900 								doout("getcode", line, top())
   901 							else
   902 								doout("gettext", line, top())
   903 							end
   904 						end
   905 						popif("emphasis", "head", "link", "code")
   906 					end
   907 				end
   908 			end
   909 		end
   910 
   911 		self.previndent = self.indentlevel
   912 		self.lnr = self.lnr + 1
   913 	end
   914 
   915 	popuntil()
   916 	doout("exit")
   917 
   918 	return self.is_dynamic_content
   919 end
   920 
   921 -------------------------------------------------------------------------------
   922 --
   923 --	parser = Markup:new(args): Creates a new markup parser. {{args}} can be a
   924 --	table of initial attributes that constitute its behavior. Fields supported
   925 --	are:
   926 --
   927 --	args.indentchar || character recognized for indentation, default {{"\t"}}
   928 --	args.input      || filehandle or string, default {{io.stdin}}
   929 --	args.rdfunc     || line reader func, by default reads from {{args.input}}
   930 --	args.wrfunc     || writer func, by default {{io.stdout.write}}
   931 --	args.docname    || name of document, default {{"Manual"}}
   932 --	args.features   || string of feature codes, default {{"hespcadlint"}}
   933 --
   934 --	The parser supports the following features, which can be combined into
   935 --	a string and passed to the constructor in {{args.features}}:
   936 --
   937 --	Code || Description        || rough HTML equivalent
   938 --	h	   || Heading            || <h1>, <h2>, ...
   939 --	e    || Emphasis           || <strong>
   940 --	s    || Separator          || <hr>
   941 --	p    || Preformatted block || <pre>
   942 --	c    || Code               || <code>
   943 --	a    || Anchor, link       || <a href="...">
   944 --	d    || Definition         || <dfn>
   945 --	l    || List               || <ul>, <li>
   946 --	i    || Image              || <img>
   947 --	n    || Node header        || <a name="...">
   948 --	t    || Table              || <table>
   949 --	f    || Function           || (undocumented, internal use only)
   950 --
   951 --	After creation of the parser object, conversion starts by calling the
   952 --	method Markup:run().
   953 --
   954 -------------------------------------------------------------------------------
   955 
   956 function Markup.new(class, self)
   957 	self = self or { }
   958 	self.indentchar = self.indentchar or "\t"
   959 	self.input = self.input or stdin
   960 	self.rdfunc = self.rdfunc or
   961 		type(self.input) == "string" and rd_string or self.input.lines
   962 	self.wrfunc = self.wrfunc or function(s) stdout:write(s) end
   963 	self.features = self.features or "hespcadlint"
   964 	self.docname = self.docname or "Manual"
   965 	return Class.new(class, self)
   966 end