cortav  Diff

Differences From Artifact [a950584594]:

To Artifact [2576638f1f]:


     1      1   -- [ʞ] cortav.lua
     2      2   --  ~ lexi hale <lexi@hale.su>
     3      3   --  © AGPLv3
     4      4   --  ? reference implementation of the cortav document language
     5      5   
     6         -local ct = { render = {} }
     7         -
     8         -local function hexdump(s)
     9         -	local hexlines, charlines = {},{}
    10         -	for i=1,#s do
    11         -		local line = math.floor((i-1)/16) + 1
    12         -		hexlines[line] = (hexlines[line] or '') .. string.format("%02x ",string.byte(s, i))
    13         -		charlines[line] = (charlines[line] or '') .. ' ' .. string.gsub(string.sub(s, i, i), '[^%g ]', '\x1b[;35m·\x1b[36;1m') .. ' '
    14         -	end
    15         -	local str = ''
    16         -	for i=1,#hexlines do
    17         -		str = str .. '\x1b[1;36m' .. charlines[i] .. '\x1b[m\n' .. hexlines[i] .. '\n'
    18         -	end
    19         -	return str
    20         -end
    21         -
    22         -local function dump(o, state, path, depth)
    23         -	state = state or {tbls = {}}
    24         -	depth = depth or 0
    25         -	local pfx = string.rep('   ', depth)
    26         -	if type(o) == "table" then
    27         -		local str = ''
    28         -		for k,p in pairs(o) do
    29         -			local done = false
    30         -			local exp
    31         -			if type(p) == 'table' then
    32         -				if state.tbls[p] then
    33         -					exp = '<' .. state.tbls[p] ..'>'
    34         -					done = true
    35         -				else
    36         -					state.tbls[p] = path and string.format('%s.%s', path, k) or k
    37         -				end
    38         -			end
    39         -			if not done then
    40         -				local function dodump() return dump(
    41         -					p, state,
    42         -					path and string.format("%s.%s", path, k) or k,
    43         -					depth + 1
    44         -				) end
    45         -				-- boy this is ugly
    46         -				if type(p) ~= 'table' or
    47         -					getmetatable(p) == nil or
    48         -					getmetatable(p).__tostring == nil then
    49         -					exp = dodump()
    50         -				end
    51         -				if type(p) == 'table' then
    52         -					exp = string.format('{\n%s%s}', exp, pfx)
    53         -					local meta = getmetatable(p)
    54         -					if meta then
    55         -						if meta.__tostring then
    56         -							exp = tostring(p)
    57         -						end
    58         -						if meta.__name then
    59         -							exp = meta.__name .. ' ' .. exp
    60         -						end
    61         -					end
    62         -				end
    63         -			end
    64         -			str = str .. pfx .. string.format("%s = %s\n", k, exp)
    65         -		end
    66         -		return str
    67         -	elseif type(o) == "string" then
    68         -		return string.format('“%s”', o)
    69         -	else
    70         -		return tostring(o)
    71         -	end
    72         -end
    73         -
    74         -local function
    75         -lerp(t, a, b)
    76         -	return (1-t)*a + (t*b)
    77         -end
    78         -
    79         -local function
    80         -startswith(str, pfx)
    81         -	return string.sub(str, 1, #pfx) == pfx
    82         -end
    83         -
    84         -local function declare(c)
    85         -	local cls = setmetatable({
    86         -		__name = c.ident;
    87         -	}, {
    88         -		__name = 'class';
    89         -		__tostring = function() return c.ident or '(class)' end;
    90         -	})
    91         -
    92         -	cls.__call = c.call
    93         -	cls.__index = function(self, k)
    94         -		if c.default and c.default[k] then
    95         -			return c.default[k]
    96         -		end
    97         -		if k == 'clone' then
    98         -			return function(self)
    99         -				local new = cls.mk()
   100         -				for k,v in pairs(self) do
   101         -					new[k] = v
   102         -				end
   103         -				if c.clonesetup then
   104         -					c.clonesetup(new, self)
   105         -				end
   106         -				return new
   107         -			end
   108         -		elseif k == 'to' then
   109         -			return function(self, to, ...)
   110         -				if to == 'string' then return tostring(self)
   111         -				elseif to == 'number' then return tonumber(self)
   112         -				elseif to == 'int' then return math.floor(tonumber(self))
   113         -				elseif c.cast and c.cast[to] then
   114         -					return c.cast[to](self, ...)
   115         -				elseif type(to) == 'table' and getmetatable(to) and getmetatable(to).cvt and getmetatable(to).cvt[cls] then
   116         -				else error((c.ident or 'class') .. ' is not convertible to ' .. (type(to) == 'string' and to or tostring(to))) end
   117         -			end
   118         -		end
   119         -		if c.fns then return c.fns[k] end
   120         -	end
   121         -
   122         -	if c.cast then
   123         -		if c.cast.string then
   124         -			cls.__tostring = c.cast.string
   125         -		end
   126         -		if c.cast.number then
   127         -			cls.__tonumber = c.cast.number
   128         -		end
   129         -	end
   130         -
   131         -	cls.mk = function(...)
   132         -		local val = setmetatable(c.mk and c.mk(...) or {}, cls)
   133         -		if c.init then
   134         -			for k,v in pairs(c.init) do
   135         -				val[k] = v
   136         -			end
   137         -		end
   138         -		if c.construct then
   139         -			c.construct(val, ...)
   140         -		end
   141         -		return val
   142         -	end
   143         -	getmetatable(cls).__call = function(_, ...) return cls.mk(...) end
   144         -	cls.is = function(o) return getmetatable(o) == cls end
   145         -	return cls
   146         -end
   147         -ct.exn = declare {
   148         -	ident = 'exn';
   149         -	mk = function(kind, ...)
   150         -		return {
   151         -			vars = {...};
   152         -			kind = kind;
   153         -		}
   154         -	end;
   155         -	cast = {
   156         -		string = function(me)
   157         -			return me.kind.report(table.unpack(me.vars))
   158         -		end;
   159         -	};
   160         -	fns = {
   161         -		throw = function(me) error(me) end;
   162         -	}
   163         -}
   164         -ct.exnkind = declare {
   165         -	ident = 'exn-kind';
   166         -	mk = function(desc, report)
   167         -		return {
   168         -			desc = desc;
   169         -			report = report or function(msg,...)
   170         -				return string.format(msg,...)
   171         -			end;
   172         -		}
   173         -	end;
   174         -	call = function(me, ...)
   175         -		return ct.exn(me, ...)
   176         -	end;
   177         -}
            6  +local ss = require 'sirsem'
            7  +-- aliases for commonly used sirsem funcs
            8  +local startswith = ss.str.begins
            9  +local eachcode = ss.str.enc.utf8.each
           10  +local dump = ss.dump
           11  +local declare = ss.declare
           12  +
           13  +-- make this module available to require() when linked into a lua bytecode program with luac
           14  +local ct = ss.namespace 'cortav'
           15  +ct.render = {}
   178     16   
   179     17   ct.exns = {
   180         -	tx = ct.exnkind('translation error', function(msg,...)
           18  +	tx = ss.exnkind('translation error', function(msg,...)
   181     19   		return string.format("(%s:%u) "..msg, ...)
   182     20   	end);
   183         -	io = ct.exnkind('IO error', function(msg, ...)
           21  +	io = ss.exnkind('IO error', function(msg, ...)
   184     22   		return string.format("<%s %s> "..msg, ...)
   185     23   	end);
   186         -	cli = ct.exnkind 'command line parse error';
   187         -	mode = ct.exnkind('bad mode', function(msg, ...)
           24  +	cli = ss.exnkind 'command line parse error';
           25  +	mode = ss.exnkind('bad mode', function(msg, ...)
   188     26   		return string.format("mode “%s” "..msg, ...)
   189     27   	end);
   190         -	unimpl = ct.exnkind 'feature not implemented';
           28  +	unimpl = ss.exnkind 'feature not implemented';
           29  +	ext = ss.exnkind 'extension error';
   191     30   }
   192     31   
   193     32   ct.ctx = declare {
   194     33   	mk = function(src) return {src = src} end;
   195     34   	ident = 'context';
   196     35   	cast = {
   197     36   		string = function(me)
................................................................................
   306    145   		secorder = {};
   307    146   		embed = {};
   308    147   		meta = {};
   309    148   		vars = {};
   310    149   	} end;
   311    150   }
   312    151   
   313         -local function map(fn, lst)
   314         -	local new = {}
   315         -	for k,v in pairs(lst) do
   316         -		table.insert(new, fn(v,k))
   317         -	end
   318         -	return new
   319         -end
   320         -local function reduce(fn, acc, lst)
   321         -	for i,v in ipairs(lst) do
   322         -		acc = fn(acc, v, i)
   323         -	end
   324         -	return acc
   325         -end
          152  +-- FP helper functions
          153  +
   326    154   local function fmtfn(str)
   327    155   	return function(...)
   328    156   		return string.format(str, ...)
   329    157   	end
   330    158   end
   331    159   
          160  +ct.ext = { loaded = {} }
          161  +function ct.ext.install(ext)
          162  +	if not ext.id then
          163  +		ct.exns.ext 'extension missing “id” field':throw()
          164  +	end
          165  +	if ct.ext.loaded[ext.id] then
          166  +		ct.exns.ext('there is already an extension with ID “%s” loaded', ext.id):throw()
          167  +	end
          168  +	ct.ext.loaded[ext.id] = ext
          169  +end
          170  +
          171  +-- renderer engines
   332    172   function ct.render.html(doc, opts)
   333    173   	local doctitle = opts['title']
   334    174   	local f = string.format
   335    175   	local ids = {}
   336    176   	local canonicalID = {}
   337    177   	local function getSafeID(obj)
   338    178   		if canonicalID[obj] then
................................................................................
   628    468   					end
   629    469   					table.insert(tb, tag('tr',nil,catenate(row)))
   630    470   				end
   631    471   				return tag('table',nil,catenate(tb))
   632    472   			end;
   633    473   			listing = function(b,s)
   634    474   				stylesNeeded.block_code_listing = true
   635         -				local nodes = map(function(l)
          475  +				local nodes = ss.map(function(l)
   636    476   					if #l > 0 then
   637    477   						return tag('div',nil,sr.htmlSpan(l, b, s))
   638    478   					else
   639    479   						return elt('hr')
   640    480   					end
   641    481   				end, b.lines)
   642    482   				if b.title then
................................................................................
   657    497   		local r = getSpanRenderers(tag,elt)
   658    498   		r.block_renderers = getBlockRenderers(tag,elt,r,catenate)
   659    499   		return r
   660    500   	end
   661    501   
   662    502   	local elt = function(t,attrs)
   663    503   		return f('<%s%s>', t,
   664         -			attrs and reduce(function(a,b) return a..b end, '', 
   665         -				map(function(v,k)
          504  +			attrs and ss.reduce(function(a,b) return a..b end, '', 
          505  +				ss.map(function(v,k)
   666    506   					if v == true
   667    507   						then          return ' '..k
   668    508   						elseif v then return f(' %s="%s"', k, v)
   669    509   					end
   670    510   				end, attrs)) or '')
   671    511   	end
   672    512   	local tag = function(t,attrs,body)
................................................................................
   836    676   				return tone(tfg,nil,nil,tonumber(alpha))
   837    677   			elseif var == 'tone' then
   838    678   				local l, sep, sat
   839    679   				for i=1,3 do -- 🙄
   840    680   					l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$')
   841    681   					if l then break end
   842    682   				end
   843         -				l = lerp(tonumber(l), tbg, tfg)
          683  +				l = ss.math.lerp(tonumber(l), tbg, tfg)
   844    684   				return tone(l, tonumber(sat), tonumber(sep), tonumber(alpha))
   845    685   			end
   846    686   		end
   847    687   		css = css:gsub('@(%w+)/([0-9.]+)(%b())', replace)
   848    688   		css = css:gsub('@(%w+)(%b())', function(a,b) return replace(a,nil,b) end)
   849    689   		css = css:gsub('@(%w+)/([0-9.]+)', replace)
   850    690   		css = css:gsub('@(%w+)', function(a,b) return replace(a,nil,b) end)
................................................................................
   887    727   	elseif opts.snippet then
   888    728   		return styletag .. body
   889    729   	else
   890    730   		return dr.htmlDoc(doctitle, next(head) and table.concat(head), body)
   891    731   	end
   892    732   end
   893    733   
   894         -local function eachcode(str, ascode)
   895         -	local pos = {
   896         -		code = 1;
   897         -		byte = 1;
   898         -	}
   899         -	return function()
   900         -		if pos.byte > #str then return nil end
   901         -		local thischar = utf8.codepoint(str, pos.byte)
   902         -		local lastpos = {
   903         -			code = pos.code;
   904         -			byte = pos.byte;
   905         -			next = pos;
   906         -		}
   907         -		if not ascode then
   908         -			thischar = utf8.char(thischar)
   909         -			pos.byte = pos.byte + #thischar
   910         -		else
   911         -			pos.byte = pos.byte + #utf8.char(thischar)
   912         -		end
   913         -		pos.code = pos.code + 1
   914         -		return thischar, lastpos
   915         -	end
   916         -end
   917         -
   918    734   do -- define span control sequences
   919    735   	local function formatter(sty)
   920    736   		return function(s,c)
   921    737   			return {
   922    738   				kind = 'format';
   923    739   				style = sty;
   924    740   				spans = ct.parse_span(s, c);
................................................................................
   946    762   				raw = raw;
   947    763   				var = not pos and s or nil;
   948    764   				origin = c:clone();
   949    765   			}
   950    766   		end
   951    767   	end
   952    768   	ct.spanctls = {
   953         -		{seq = '$', parse = formatter 'literal'};
   954    769   		{seq = '!', parse = formatter 'emph'};
   955    770   		{seq = '*', parse = formatter 'strong'};
          771  +		{seq = '~', parse = formatter 'strike'};
          772  +		{seq = '+', parse = formatter 'inser'};
   956    773   		{seq = '\\', parse = function(s, c) -- raw
   957    774   			return s
   958    775   		end};
   959    776   		{seq = '$\\', parse = function(s, c) -- raw
   960    777   			return {
   961    778   				kind = 'format';
   962    779   				style = 'literal';
   963    780   				spans = {s};
   964    781   				origin = c:clone();
   965    782   			}
   966    783   		end};
          784  +		{seq = '$', parse = formatter 'literal'};
   967    785   		{seq = '&', parse = function(s, c)
   968    786   			local r, t = s:match '^([^%s]+)%s*(.-)$'
   969    787   			return {
   970    788   				kind = 'term';
   971    789   				spans = (t and t ~= "") and ct.parse_span(t, c) or {};
   972    790   				ref = r; 
   973    791   				origin = c:clone();
................................................................................
   988    806   		{seq = '##', parse = insert_var_ref(true)};
   989    807   		{seq = '#', parse = insert_var_ref(false)};
   990    808   	}
   991    809   end
   992    810   
   993    811   function ct.parse_span(str,ctx)
   994    812   	local function delimited(start, stop, s)
   995         -		local depth = 0
   996         -		if not startswith(s, start) then return nil end
   997         -		for c,p in eachcode(s) do
   998         -			if c == '\\' then
   999         -				p.next.byte = p.next.byte + #utf8.char(utf8.codepoint(s, p.next.byte))
  1000         -				p.next.code = p.next.code + 1
  1001         -			elseif c == start then
  1002         -				depth = depth + 1
  1003         -			elseif c == stop then
  1004         -				depth = depth - 1
  1005         -				if depth == 0 then
  1006         -					return s:sub(1+#start, p.byte - #stop), p.byte -- FIXME
  1007         -				elseif depth < 0 then
  1008         -					ctx:fail('out of place %s', stop)
  1009         -				end
  1010         -			end
  1011         -		end
  1012         -
  1013         -		ctx:fail('[%s] expected before end of line', stop)
          813  +		-- local r = { pcall(ss.str.delimit, 'utf8', start, stop, s) }
          814  +		-- if r[1] then return table.unpack(r, 2) end
          815  +		-- ctx:fail(tostring(e))
          816  +		return ss.str.delimit(ss.str.enc.utf8, start, stop, s)
  1014    817   	end
  1015    818   	local buf = ""
  1016    819   	local spans = {}
  1017    820   	local function flush()
  1018    821   		if buf ~= "" then
  1019    822   			table.insert(spans, buf)
  1020    823   			buf = ""
................................................................................
  1077    880   		else
  1078    881   			buf = buf .. c
  1079    882   		end
  1080    883   	end
  1081    884   	flush()
  1082    885   	return spans
  1083    886   end
  1084         -
  1085    887   
  1086    888   local function
  1087    889   blockwrap(fn)
  1088    890   	return function(l,c)
  1089    891   		local block = fn(l,c)
  1090    892   		block.origin = c:clone();
  1091    893   		table.insert(c.sec.blocks, block);
................................................................................
  1166    968   	end;
  1167    969   }
  1168    970   
  1169    971   local function insert_table_row(l,c)
  1170    972   	local row = {}
  1171    973   	local buf
  1172    974   	local flush = function()
  1173         -		if buf then table.insert(row, buf) end
          975  +		if buf then
          976  +			buf.str = buf.str:gsub('%s+$','')
          977  +			table.insert(row, buf)
          978  +		end
  1174    979   		buf = { str = '' }
  1175    980   	end
  1176    981   	for c,p in eachcode(l) do
  1177    982   		if c == '|' or c == '+' and (p.code == 1 or l:sub(p.byte-1,p.byte-1)~='\\') then
  1178    983   			flush()
  1179    984   			buf.header = c == '+'
  1180    985   		elseif c == ':' then
................................................................................
  1383   1188   				end
  1384   1189   			end
  1385   1190   		end
  1386   1191   	end
  1387   1192   
  1388   1193   	return ctx.doc
  1389   1194   end
  1390         -
  1391         -local default_mode = {
  1392         -	['render:format'] = 'html';
  1393         -	['html:gen-styles'] = true;
  1394         -}
  1395         -
  1396         -local function filter(list, fn)
  1397         -	local new = {}
  1398         -	for i, v in ipairs(list) do
  1399         -		if fn(v,i) then table.insert(new, v) end
  1400         -	end
  1401         -	return new
  1402         -end
  1403         -
  1404         -local function kmap(fn, list)
  1405         -	local new = {}
  1406         -	for k, v in pairs(list) do
  1407         -		local nk,nv = fn(k,v)
  1408         -		new[nk or k] = nv or v
  1409         -	end
  1410         -	return new
  1411         -end
  1412         -local function kfilter(list, fn)
  1413         -	local new = {}
  1414         -	for k, v in pairs(list) do
  1415         -		if fn(k,v) then new[k] = v end
  1416         -	end
  1417         -	return new
  1418         -end
  1419         -
  1420         -local function main(input, output, log, mode, vars)
  1421         -	local doc = ct.parse(input.stream, input.src, mode)
  1422         -	input.stream:close()
  1423         -	if mode['parse:show-tree'] then
  1424         -		log:write(dump(doc))
  1425         -	end
  1426         -
  1427         -	if not mode['render:format'] then
  1428         -		error 'what output format should i translate the input to?'
  1429         -	end
  1430         -	if mode['render:format'] == 'none' then return 0 end
  1431         -	if not ct.render[mode['render:format']] then
  1432         -		ct.exns.unimpl('output format “%s” unsupported', mode['render:format']):throw()
  1433         -	end
  1434         -	
  1435         -	local render_opts = kmap(function(k,v)
  1436         -		return k:sub(2+#mode['render:format'])
  1437         -	end, kfilter(mode, function(m)
  1438         -		return startswith(m, mode['render:format']..':')
  1439         -	end))
  1440         -
  1441         -	doc.vars = vars
  1442         -	
  1443         -	-- this is kind of gross but the context object belongs to the parser,
  1444         -	-- not the renderer, so that's not a suitable place for this information
  1445         -	doc.stage = {
  1446         -		kind = 'render';
  1447         -		format = mode['render:format'];
  1448         -		mode = mode;
  1449         -	}
  1450         -
  1451         -	output:write(ct.render[mode['render:format']](doc, render_opts))
  1452         -	return 0
  1453         -end
  1454         -
  1455         -local inp,outp,log = io.stdin, io.stdout, io.stderr
  1456         -
  1457         -local function entry_cli()
  1458         -	local mode, vars, input = default_mode, {}, {
  1459         -		stream = inp;
  1460         -		src = {
  1461         -			file = '(stdin)';
  1462         -		}
  1463         -	}
  1464         -
  1465         -	local optnparams = function(o)
  1466         -		local param_opts = {
  1467         -			out = 1;
  1468         -			log = 1;
  1469         -			define = 2; -- key value
  1470         -			['mode-set'] = 1;
  1471         -			['mode-clear'] = 1;
  1472         -			mode = 2;
  1473         -		}
  1474         -		return param_opts[o] or 0
  1475         -	end
  1476         -
  1477         -	local optmap = {
  1478         -		o = 'out';
  1479         -		l = 'log';
  1480         -		d = 'define';
  1481         -		V = 'version';
  1482         -		h = 'help';
  1483         -		y = 'mode-set', n = 'mode-clear';
  1484         -		m = 'mode';
  1485         -	}
  1486         -
  1487         -	local checkmodekey = function(key)
  1488         -		if not key:match '[^:]+:.+' then
  1489         -			ct.exns.cli('invalid mode key %s', key):throw()
  1490         -		end
  1491         -		return key
  1492         -	end
  1493         -	local onswitch = {
  1494         -		out = function(file)
  1495         -			local nf = io.open(file,'wb')
  1496         -			if nf then outp:close() outp = nf else
  1497         -				ct.exns.io('could not open output file for writing', 'open',file):throw()
  1498         -			end
  1499         -		end;
  1500         -		log = function(file)
  1501         -			local nf = io.open(file,'wb')
  1502         -			if nf then log:close() log = nf else
  1503         -				ct.exns.io('could not open log file for writing', 'open',file):throw()
  1504         -			end
  1505         -		end;
  1506         -		define = function(key,value)
  1507         -			if startswith(key, 'cortav.') or startswith(key, 'env.') then
  1508         -				ct.exns.cli 'cannot define variable in restricted namespace':throw()
  1509         -			end
  1510         -			vars[key] = value
  1511         -		end;
  1512         -		mode = function(key,value) mode[checkmodekey(key)] = value end;
  1513         -		['mode-set'] = function(key) mode[checkmodekey(key)] = true end;
  1514         -		['mode-clear'] = function(key) mode[checkmodekey(key)] = false end;
  1515         -	}
  1516         -
  1517         -	local args = {}
  1518         -	local keepParsing = true
  1519         -	do local i = 1 while i <= #arg do local v = arg[i]
  1520         -		local execLongOpt = function(longopt)
  1521         -			if not onswitch[longopt] then
  1522         -				ct.exns.cli('switch --%s unrecognized', longopt):throw()
  1523         -			end
  1524         -			local nargs = optnparams(longopt)
  1525         -
  1526         -			if nargs > 1 then
  1527         -				if i + nargs > #arg then
  1528         -					ct.exns.cli('not enough arguments for switch --%s (%u expected)', longopt, nargs):throw()
  1529         -				end
  1530         -				local nt = {}
  1531         -				for j = i+1, i+nargs do
  1532         -					table.insert(nt, arg[j])
  1533         -				end
  1534         -				onswitch[longopt](table.unpack(nt))
  1535         -			elseif nargs == 1 then
  1536         -				onswitch[longopt](arg[i+1])
  1537         -			end
  1538         -			i = i + nargs
  1539         -		end
  1540         -		if v == '--' then
  1541         -			keepParsing = false
  1542         -		else
  1543         -			local longopt = v:match '^%-%-(.+)$'
  1544         -			if keepParsing and longopt then
  1545         -				execLongOpt(longopt)
  1546         -			else
  1547         -				if keepParsing and v:sub(1,1) == '-' then
  1548         -					for c,p in eachcode(v:sub(2)) do
  1549         -						if optmap[c] then
  1550         -							execLongOpt(optmap[c])
  1551         -						else
  1552         -							ct.exns.cli('switch -%i unrecognized', c):throw()
  1553         -						end
  1554         -					end
  1555         -				else
  1556         -					table.insert(args, v)
  1557         -				end
  1558         -			end
  1559         -
  1560         -		end
  1561         -	i = i + 1 end end
  1562         -
  1563         -	if args[1] and args[1] ~= '' then
  1564         -		local file = io.open(arg[1], "rb")
  1565         -		if not file then error('unable to load file ' .. args[1]) end
  1566         -		input.stream = file
  1567         -		input.src.file = args[1]
  1568         -	end
  1569         -
  1570         -	return main(input, outp, log, mode, vars)
  1571         -end
  1572         -
  1573         -local ok, e = pcall(entry_cli)
  1574         --- local ok, e = true, entry_cli()
  1575         -if not ok then
  1576         -	local str = 'translation failure'
  1577         -	if ct.exn.is(e) then
  1578         -		str = e.kind.desc
  1579         -	end
  1580         -	local color = false
  1581         -	if log:seek() == nil then
  1582         -		-- this is not a very reliable heuristic for detecting
  1583         -		-- attachment to a tty but it's better than nothing
  1584         -		if os.getenv('COLORTERM') then
  1585         -			color = true
  1586         -		else
  1587         -			local term = os.getenv('TERM')
  1588         -			if term:find 'color' then color = true end
  1589         -		end
  1590         -	end
  1591         -	if color then
  1592         -		str = string.format('\27[1;31m%s\27[m', str)
  1593         -	end
  1594         -	log:write(string.format('%s: %s\n', str, e))
  1595         -	os.exit(1)
  1596         -end
  1597         -os.exit(e)