Die Dokumentation für dieses Modul kann unter Modul:Benutzer:Vollbracht/Literatur/Doku erstellt werden

--[=[ Literatur 2022-09-27
Module for bibliography reuse and ordered aggregation
Author: Vollbracht

Service functions
* byWikiData(qualifier)	single bibliography item
* byParamSet(table)		single bibliography item
* ISBN(source)			test and format isbn

Service object
* dataset		universal literature data container
	constructor:
		new(source)
	fields:		depending on source; field names are given as values in
				transCode tables
	methods:
		satisfyConstraints
					declares information to be redundant or irrelevant
		condAdd		adds objects to this dataSet with respect to constraints
		addList		adds an array of names to this dataSet
		addSet		conditionally adds a transcoded sequence of objects
		bequeath	inherit transcoded data from a more general dataset
		processSingle	tostring a singular dataSet element
		processList		tostring a dataSet array element
		processObject	tostring a dataset object element
		processSet		tostring a sequence of dataSet elements
		toString	do not use externally. This method will be renamed to
					__tostring
		__tostring	not available yet for the sake of better debugging

template functions
* byWikiData(frame, qualifier, position)	single bibliography entry
* ISBN(source)								format isbn
* list(frame, <unnamed content string>)		bibliography in list form
* listEntry(frame)							entry as part of list content string
* anchor(frame)								bibliography entry with anchor
]=]

--Module globals
local p = {service = {}}

local _, Parser = pcall(require, "Modul:SimpleStruct")
local _, Limited = pcall(require, "Modul:SimpleDataAccess")
local _, bit = pcall(require, 'bit32' )
local _, Titles = pcall(require, "Modul:Title")
local _, Time = pcall(require, "Modul:Time")
local _, URL = pcall(require, "Modul:URL")
Titles = Titles.service
Time = Time.point

-- Wikipedia:Lua/Modul/URIutil functions seam unfeasible for ISBN handling.

-- Don't mention German language in deWiki! (It's expected.)
local PREF_LANG = 'Deutsch'
-- Don't mention persons if their number excedes a certain maximum!
local maxPersons = {
	P50=7, P2093=7, authors=7,	-- authors (up to 7 + 7)
	P767=3, coauthors=3,		-- coauthors (up to 3)
	P98=7, P5769=7, editors=7	-- editors (up to 7 + 7)
}

-- pairs of <source key>, <drain key>
local transCode = {
	-- all properties that could be copied from book data
	tcP1476 = {
		authors='authors', title='title', subtitle='subtitle',
		contribution='contribution', editors='editors', partOf='partOf',
		series='series', seriesContribution='seriesContribution',
		volume='volume', seriesVolume='seriesVolume', number='number',
		seriesNumber='seriesNumber', edition='edition', language='language',
		NoPages='NoPages', publisher='publisher', PoPub='PoPub', year='year',
		ISBN='ISBN', ISSN='ISSN', ZDB='ZDB', DNB='DNB', LCCN='LCCN', OCLC='OCLC',
		ID='ID', Lizenznummer='Lizenznummer', arXiv='arXiv', bibcode='bibcode',
		DOI='DOI', archive='archive', JSTOR='JSTOR', PMC='PMC', PMID='PMID',
		article='article', URL='URL', archive='archive', Format='fileFormat',
		KBytes='dataSize', dataLink='dataLink', originalLanguage,
		translator='translator', URN='URN'
	},
	-- all properties that could be copied from partOf data
	tcP361 = {
		authors='authors', editors='editors', title='partOf', partOf='partOf',
		volume='volume', number='number', series='series',
		seriesVolume='seriesVolume', seriesNumber='seriesNumber',
		ISBN='ISBN', ISSN='ISSN', ZDB='ZDB', DNB='DNB', LCCN='LCCN', OCLC='OCLC',
		Lizenznummer='Lizenznummer', DOI='DOI', URL='URL', ID='ID', URN='URN',
		edition='edition', publisher='publisher', PoPub='PoPub', year='year',
		dataLink='dataLink'
	},
	tcP1433 = {
		authors='authors', editors='editors', title='partOf', partOf='partOf',
		volume='volume', number='number', series='series',
		seriesVolume='seriesVolume', seriesNumber='seriesNumber',
		ISBN='ISBN', ISSN='ISSN', ZDB='ZDB', DNB='DNB', LCCN='LCCN', URL='URL',
		OCLC='OCLC', Lizenznummer='Lizenznummer', DOI='DOI', URN='URN', ID='ID',
		edition='edition', publisher='publisher', PoPub='PoPub', year='year',
		dataLink='dataLink'
	},
	-- all properties that could be copied from series data
	tcP179 = {
		authors='authors', editors='editors', title='series', partOf='series',
		volume='seriesVolume', number='seriesNumber',
		ZDB='ZDB', DNB='DNB', LCCN='LCCN', OCLC='OCLC',
		Lizenznummer='Lizenznummer', DOI='DOI', URN='URN', ID='ID',
		edition='edition', publisher='publisher', PoPub='PoPub', year='year',
		dataLink='dataLink'
	},
	-- all properties that could occur in German templates
	tcGer = {
		Autor='authors', Autoren='authors', Titel='title',
		Untertitel='subtitle', TitelErg='contribution', Hrsg='editors',
		Herausgeber='editors', HrsgReihe='editors', Sammelwerk='partOf',
		Reihe='series', WerkErg='seriesContribution',
		Band='volume', BandReihe='seriesVolume', Nummer='number',
		NummerReihe='seriesNumber',
		Auflage='edition',    
		Sprache='language', Umfang='NoPages',
		Verlag='publisher', Ort='PoPub', Datum='year',
		ISBN='ISBN', ISBNformalFalsch='ISBN', ISBNdefekt='ISBN', ISSN='ISSN',
		ISSNformalFalsch='ISSN', ZDB='ZDB', DNB='DNB', LCCN='LCCN', OCLC='OCLC',
		Lizenznummer='Lizenznummer', arXiv='arXiv', bibcode='bibcode',
		DOI='DOI', JSTOR='JSTOR', PMC='PMC', PMID='PMID', URN='URN', ID='ID',
		Kapitel='chapter', Seite='page', Seiten='page', Spalten='column',
    	ArtikelNr='article', Fundstelle='PoFind', Kommentar='comment',
    	Online='URL', Format='fileFormat', KBytes='dataSize', Abruf='retrieved',
    	Originaltitel='originalTitle',  Originalsprache='originalLanguage',
    	Originaljahr='retrieved', ['Übersetzer']='translator'
	},
	-- all literature properties known in wikiData
	tcP = {
		P50='authors', P2093='authors', P767='contributor', P1476 = 'title',
		P1680='subtitle', P98='editors', P5769='editors', P361='partOf',
		P1433='partOf', P179='series', P478='volume', P1545='number',
		P433='number', P393='edition', P9767='edition', P407='language', P1104='NoPages',
		P123='publisher', P291='PoPub', P577='year',
		P957='ISBN', P212='ISBN', P236='ISSN', P1042='ZDB-ID', P1292='DNB',
		P1144='LCCN', P243='OCLC', P5331='OCLC', P818='arXiv', P1300='bibcode',
		P356='DOI', P724='archive', P888='JSTOR', P932='PMC', P698='PMID',
		P1813='ID', P2699='URL', P2701='fileFormat', P3575='dataSize',
		P813='retrieved', P364='originalLanguage', P655='translator'
	},
	-- all legators
	tcLegator={ title='P1476', partOf='P361', series='P179' },
	-- format strings used for properies
	tcFormat = {
		-- books: (don't rely on magic words:)
		ISBN=		'[[spezial:ISBN-Suche/%s|ISBN %s]]',
		["ISBN-10"]='[[spezial:ISBN-Suche/%s|ISBN %s]]',
		["ISBN-13"]='[[spezial:ISBN-Suche/%s|ISBN %s]]',
		ISBNwrong='[[spezial:ISBN-Suche/%s|ISBN %s]] <small>(ungült.)</small>',
		ISBNinvalid='[[spezial:ISBN-Suche/%s|ISBN %s]] <small>(unmögl.)</small>',
		LCCN=		'[[Library of Congress Control Number|LCCN]] [https://lccn.'
				..	'loc.gov/%s/ %s]',
		PMID=  '[https://www.ncbi.nlm.nih.gov/pubmed/%s?dopt=Abstract PMID %s]',
		PMC=		'[https://www.ncbi.nlm.nih.gov/pmc/articles/PMC%s/ PMC %s]',
		-- positions:
		page=		'S.&nbsp;%s',
		column=		'Spalte&nbsp;%s',
		originalTitle='Originaltitel: %s',
		originalLanguage='Originalsprache: %s',
		translator='Übersetzer: %s',
		dataLink=	'[[d:%s|wd]]'
	},
	tcURLprefix = {
		DNB=		'[[Deutsche Nationalbibliothek|DNB]] [https://portal.dnb.de'
				..	'/opac.htm?referrer=Wikipedia&method=simpleSearch&cqlMode=t'
				..	'rue&query=idn%3D',
		OCLC=		'[[Online Computer Library Center|OCLC]] [https://worldcat.'
				..	'org/oclc/',
		-- journals:
		ISSN=		'[[ISSN]] [https://portal.issn.org/resource/issn/',
		ZDB=		'[[ZDB]] [https://zdb-katalog.de/title.xhtml?idn=',
		-- articles:
		DOI=		'[[Digital Object Identifier|doi]]:[https://doi.org/',
		JSTOR=		'[[JSTOR]] [https://www.jstor.org/stable/',
		-- online:
		arXiv=		'[[ArXiv|arxiv]]: [https://arxiv.org/abs/',
		archive=	'[[Internet Archive]]: [https://archive.org/details/',
		bibcode=	'[[bibcode]]: [https://ui.adsabs.harvard.edu/abs/'
	},
	tcClass = {
		year=Time,
		retrieved=Time,
		['URL']=URL
	},
	tcObjectFormat = {
		year='yyyy',
		retrieved='abgerufen am %d.%m.%Y',
		URL='online unter <domain>'
	}
}
-- for some properties lists of dependencies
local constraints={
	-- 1. positive: if <key> data available, enter under {<circumstances>}
	subtitle={title=true},
	seriesEditors={editors=false},
	language={target={neq=PREF_LANG}},
	DNB={ISBN=false},
	OCLC={ISBN=false, DNB=false},
	LCCN={ISBN=false, DNB=false, OCLC=false},
	ZDB={ISSN=false},
	URL={archive=false},
	-- 2. negative: if <key> data available, delete {<other keys>} data
	anti={
		ISBN={'DNB', 'OCLC', 'LCCN'},
		DNB={'OCLC', 'LCCN'},
		OCLC={'LCCN'},
		ISSN={'ZDB'},
		archive={'URL'}
	}
}
-- list of properties to be requested in a bunch
local identifiers={
	'P724', 'P212', 'P957', 'P236', 'P1042', 'P1292', 'P1144', 'P243',
	'P818', 'P1300', 'P356', 'P888', 'P932', 'P698', 'P1813'
}
--[[
	how to handle a property:
		1 -	(simple)	property is a single value
		2 - (list)		property is a list of values whereat , 3 - legator, 4 - generic, 5 - object
]]
local cpHandling={
	authors=2, title=3, subtitle=1, contribution=1, editors=2, partOf=3,
	series=3, seriesContribution=1, volume=1, seriesVolume=1, number=1,
	seriesNumber=1, edition=1, language=1, NoPages=1, publisher=1, PoPub=2,
	year=5, ISBN=4, ISSN=1, ZDB=1, DNB=1, LCCN=1, OCLC=1, Lizenznummer=1,
	arXiv=1, bibcode=1, DOI=1, JSTOR=1, PMC=1, PMID=1, URN=1, ID=1, chapter=1,
	page=1, column=1, article=1, PoFind=1, comment=1, URL=5, fileFormat=1,
	dataSize=1, retrieved=5, originalTitle=1, originalLanguage=1,
	originalYear=5, translator=1
}

-------------------- local functions --------------------

-- if nops met then ops not met
local nops = {
	eq = function(a, b) return a ~= b end,
	neq = function(a, b) return a == b end,
	le = function(a, b) return a > b end,
	ge = function(a, b) return a < b end,
	lt = function(a, b) return a >= b end,
	gt = function(a, b) return a <= b end,
	['nil'] = function(a, b) if a then return true end return false end
}

-------------------- service functions --------------------
-- functions to be called from within other modules
-----------------------------------------------------------

p.service.dataset = {
	--[[
		satisfyConstraints
		value if constraints are satisfied
		parameters:
			property:	designated for value; defining constraints
			value:		designated result
		returns: value if all constraints for property are met / nil otherwise
	]]
	satisfyConstraints = function(this, property, value)
		if not value then return nil end
		if value == '' then return nil end
		local cList = constraints[property]
		if not cList then return value end
		for cProp, cValue in pairs(cList) do
			if type(cValue) == 'boolean' then
				if ((this[cProp] or false) and this[cProp] ~= '') ~= cValue then
					return nil
				end
			elseif type(cValue) == 'table' then
				local comparator = value
				if cProp ~= 'target' then comparator = this[cProp] end
				for c2, cV2 in pairs(cValue) do
					if nops[c2] == nil then
						if not comparator or comparator == '' then return nil end
					end
					if nops[c2](comparator, cV2) then return nil end
				end
			else
				if this[cProp] and this[cProp] ~= cValue then return nil end
			end
		end
		return value
	end,
	--[[
		dataSet:condAdd(pName, value)
		Conditionally adds a value to a dataSet with respect to constraint set.
		Will not alter or replace a preexisting property named pName
		parameters:
			pName	property name:	value will be stored as dataSet[pName] if
									all constraints are met.
			value	property value to be stored
		returns:
			true	if property could be newly written
			false	if property has not been written
						due to preexistance
						due to contradictory constraints
						due to missing data
	]]
	condAdd = function(this, pName, value)
		if not pName then return false end
		if this[pName] then return false end
		value = this:satisfyConstraints(pName, value)
		if not value then return false end
		this[pName] = value
		cList = constraints.anti[pName]
		if cList then
			for _, ck in ipairs(cList) do this[ck] = nil end
		end
		return true
	end,
	--[[
		dataSet:addList(pName, list)
		Adds an item list (enhancement, no replacement) i. e. adds all listed
		values found in item list to a properties list.
		parameters:
			pName	property name:	values will be added to dataSet[pName] table
			list	source of new property values to be added there
					Will accept table of strings or comma separated string list.
					Will interpret Wikidata object identifiers and will fetch
					native language name or label and sitelink if available.
					Attention: will not prevent double entries ('Tom' and 'Tom')
		returns:
			true	if at least one new value could be added
			false	if no value could be added due to missing or malformed data
	]]
	addList = function(this, pName, list)
		if not pName then return false end
		if not list then return false end
		if type(list) == 'string' then list = mw.text.split(list, ',%s*')
		elseif type(list) ~= 'table' then return false end
		if not list[1] then return false end
		if not this[pName] then this[pName] = {} end
		local result = false
		--mw.logObject(list, 'list in addList for ' .. pName)
		for _, name in ipairs(list) do
			local nid = nil
			if type(name) == 'string' then nid = name:match('Q%d+') end
			if nid then
				-- prefer native language name
				local n = Limited.MainSnakValue(nid, 'P1559')
				-- if not available fall back to label
				if not n then n = mw.wikibase.getLabel(nid) end
				local l = mw.wikibase.getSitelink(nid)
				if l then
					if not n or n == '' then n = '[[d:' .. nid .. '|n. n.]]'
					elseif l == n then n = '[[' .. l .. ']]'
					else n = '[[' .. l .. '|' .. n .. ']]' end
				end
				nid = n
				table.insert(this[pName], nid)
				result = true
			elseif name ~= '' then
				if type(name) == 'string' then
					table.insert(this[pName], name)
					result = true
				else
					mw.logObject(name, 'falscher Datentyp')
					mw.log(name)
				end
			end
		end
		--mw.logObject(this, 'dataSet after addList')
		return result
	end,
	--[[
		dataSet:addSet(source, tcName)
		Conditionally copies all elements defined in transcode list out of
		source. Transcode list is a list of key=value pairs whereat key is
		source key and value is drain key (key in this object). Will not copy an
		element if this[drain key] is present already.
		parameters:
			source	a dataSet or any structure containing usefull key=value
					pairs
			tcName	name of a transcode list that can be retreived as
					transCode[tcName]
		no return value
	]]
	addSet = function(this, source, tcName)
		local tc = transCode[tcName]
		for k, v in pairs(source) do
			local tk = tc[k]
			if tk then
				this:condAdd(tk, v)
			end
		end
	end,
	--[[ --------------------- legator handling ---------------------
		1. add a struct containing title and optionally volume and number
		2. refine the struct after retaining all primary information
		3. copy refined secondary information into dataSet
	]]
	bequeath = function(this, pName, fbVolume, fbNumber)
		if not pName then return false end
		local ids = this[pName]
		if not ids then return false end
		local id = ids:match('Q%d+')
		if not id then return false end
		local legator = p.service.byWikiData(id)
		if legator then
			this[pName] = nil
			this:addSet(legator, 'tc' .. pName)
			this:addSet({volume=fbVolume, number=fbNumber}, 'tc' .. pName)
			return true
		else
			this:condAdd(transCode.tcP[pName],
				'[[d:' .. id .. '|<span style="color:red;">' .. id
			..	' ohne Titel</span>]]')
			return false
		end
	end,
	--[[ --------------------- output handling ---------------------
		dataSet:processSingle(pName, isNotFirst, prefix, postfix)
		Extends this.stringVal by formated single value of a single property.
		Will append prefix and postfix if property value is present only.
		parameters:
			pName		property name (optional, don't add anything if absent)
			isNotFirst	Is a punctuation pending already? (true, false or nil)
			prefix		string value to be prepended to property value
						(optional; default by isNotFirst)
			postfix		string value to be postpended to property value
		returns new isNotFirst value
	]]
	processSingle = function(this, pName, isNotFirst, prefix, postfix)
		if not pName then return isNotFirst end
		if not prefix then prefix = '' end
		if not postfix then postfix = '' end
		local val = this[pName]
		if not val then return isNotFirst end
		if isNotFirst and not prefix:find('^%p') then
			this.stringVal = this.stringVal .. ','
		end
		this.stringVal = this.stringVal .. prefix .. val .. postfix
		if postfix:match('%p%s*$') then return false end
		return true
	end,
	--[[
		dataSet:processList(pName, prefix, separator, postfix)
		Extends this.stringVal by formated list of values of a single property.
		Will append prefix and postfix only if property is present with at least
		one list element.
		parameters:
			pName		property name (optional, don't add anything if absent)
			prefix		string value to be prepended to the list
						(optional; default by isNotFirst)
			separator	string value to be inserted in between the list values
			postfix		string value to be postpended to the list
		returns new isNotFirst value
	]]
	processList = function(this, pName, prefix, separator, postfix)
		if this[pName] then
			if prefix then
				if type(prefix) ~= 'string' then
					this.stringVal = this.stringVal .. ', '
				else
					this.stringVal = this.stringVal .. prefix
				end
			elseif prefix ~= nil then
				this.stringVal = this.stringVal .. ' '
			end
			if not separator then separator = ', ' end
			this.stringVal =	this.stringVal
							..	table.concat(this[pName], separator)
			if postfix then
				this.stringVal = this.stringVal .. postfix
				if postfix:match('%.') then return false end
			end
			return true
		else return false end
	end,
	processObject = function(this, pName, prefix, postfix)
		local value = this[pName]
		if value then
			if prefix then
				if type(prefix) ~= 'string' then
					this.stringVal = this.stringVal .. ', '
				else
					this.stringVal = this.stringVal .. prefix
				end
			elseif prefix ~= nil then
				this.stringVal = this.stringVal .. ' '
			end
			if not postfix then postfix = '' end
			value = value:format(transCode.tcObjectFormat[pName])
			this.stringVal = this.stringVal .. value .. postfix
			if postfix:match('%.') then return false end
			return true
		end
		if type(prefix) ~= 'string' then return prefix end
		return prefix:find('^,')
	end,
	processSet = function(this, list, prefix, separator, postfix)
		if list then
			local l = {}
			for _, pName in ipairs(list) do
				local n = this[pName]
				if n then
					local fmtString = transCode.tcFormat[pName]
					if fmtString then
						--mw.log(fmtString .. ' / ' .. n)
						table.insert(l, string.format(fmtString, n, n))
					else
						fmtString = transCode.tcObjectFormat[pName]
						if fmtString then
							table.insert(l, n:format(fmtString))
						else
							fmtString = transCode.tcURLprefix[pName]
							if fmtString then
								table.insert(l, fmtString .. n .. ' ' .. n .. ']')
							else
								table.insert(l, n)
							end
						end
					end
				end
			end
			if #l > 0 then list = l
			else list = nil end
		end
		if not list then
			if prefix then
				if type(prefix) == 'string' then return false end
				return true
			end
			return false
		end
		local isNotFirst = false
		if prefix then
			if type(prefix) ~= 'string' then
				this.stringVal = this.stringVal .. ', '
			else
				this.stringVal = this.stringVal .. prefix
			end
		elseif prefix ~= nil then
			this.stringVal = this.stringVal .. ' '
		end
		if not separator then separator = ', ' end
		for _, value in ipairs(list) do
			if isNotFirst then
				this.stringVal = this.stringVal .. separator
			end
			this.stringVal = this.stringVal .. value
			isNotFirst = true
		end
		if postfix then
			this.stringVal = this.stringVal .. postfix
			if postfix:match('%.') then return false end
		end
		return true
	end
}
function p.service.dataset:new (source)
	local result = {}
	if type(source) == 'string' then
		result.dataLink = source:match('Q%d+')
	end
	result.stringVal = ''
	setmetatable(result, self)
	self.__index = self
	return result
end

--[[
	dataSet:toString()
	formating a dataset into a wikitext: puting preformated text in order
	will be renamed to __tostring()
	parameters:
		dataSet: bibliographical as
		{authors={<name>, ...}, title=<title>,
		 editors={<name>, ...}, series=<name>, volume=<text>,
		 publisher=<name>, PoPub=<point of publishing>, year=<text>,
		 ISBN=<formated number>, DNB=<number>, DOI=<number>,
		 page=<number>, column=<number>...}
	returns:	citation string
]]
p.service.dataset.toString = function(this)
	mw.logObject(this)
	local dot = false
	fullstop = function()
		if dot then
			this.stringVal = this.stringVal .. '.'
			dot = false
		end
	end
	addEdition = function()
		if this.edition then
			if this.edition:match('^[%d]+$') then
				dot = this:processSingle('edition', dot, " ", '. Auflage')
			elseif this.edition:match('Auflage') then
				if this.edition:match('%.$') == '.' then
					this:processSingle('edition', dot, " ")
					dot=false
				else
					dot = this:processSingle('edition', dot, " ")
				end
			else
				if this.edition:match('%.$') == '.' then
					dot = this:processSingle('edition', dot, " ", ' Auflage')
				else
					dot = this:processSingle('edition', dot, " Auflage: ")
				end
			end
		end
	end
	this.stringVal = ''
	local scheme = 0
	local sProps = {'volume', 'number', 'seriesVolume', 'seriesNumber',
					'authors', 'partOf', 'series', 'editors'}
	for i, v in ipairs(sProps) do
		if this[v] and this[v] ~= '' then
			scheme = bit.bor(scheme, bit.lshift(1, i))
		end
	end
	scheme = bit.rshift(scheme, 1)
	if not this:processList('authors', '', ', ', ": ") then
		this:processList('editors', '', ', ', " (Hrsg.): ")
	end
	if		this.subtitle and this.subtitle ~= ''
		and not this.subtitle:find("^%s*%'%'") then
			this.subtitle = "''" .. this.subtitle .. "''"
	end
	dot = this:processSet({'title', 'subtitle'}, "", ': ', "")
	if this.contribution and this.contribution ~= '' then
		this.stringVal = this.stringVal .. ' ' .. this.contribution
	end -- dot pending
	if bit.band(scheme, 176) == 176 then -- author, editor, partOf, ...
		this:processList('editors', '. In:&nbsp;', ', ', " (Hrsg.): ")
		dot = this:processSingle('partOf', false)
		addEdition()
	elseif bit.band(scheme, 32) == 32 then -- no author or no editor
		dot = this:processSingle('partOf', false, ". In:&nbsp;", "")
		addEdition()
	elseif scheme == 208 then -- author, editor, series.
		addEdition()
		this:processList('editors', '. In:&nbsp;', ', ', " (Hrsg.): ")
		dot = this:processSingle('series', false)
	elseif bit.band(scheme, 111) == 64 then -- series only
		addEdition()
		dot = this:processSingle('series', false, ". In:&nbsp;", "")
	else
		addEdition()
	end
	if bit.band(scheme, 65) == 1 then
		dot = this:processSet({'volume', 'number'}, ', Band ', '.')
	elseif bit.band(scheme, 66) == 2 then
		dot = this:processSingle('number', dot, ", Nummer ", '.')
	elseif bit.band(scheme,44) > 0 then
		if bit.band(scheme, 1) ~= 0 then
			dot = this:processSet({'volume', 'number'}, ', Band ', '.')
		elseif bit.band(scheme, 2) > 0 then
			dot = this:processSingle('number', dot, ", Nummer ")
		end
	end
	if bit.band(scheme, 243) == 146 then
		this:processList('editors', " Hrsg.: ", ', ', '.')
		dot = false
	elseif bit.band(scheme, 240) == 144 then
		this:processList('editors', ". Hrsg.: ", ', ', '.')
		dot = false
	elseif bit.band(scheme, 111) > 64 then
		this.stringVal = this.stringVal .. ' (=&nbsp;'
		if bit.band(scheme, 240) == 208 then
			this:processList('editors', '', ', ', " (Hrsg.): ")
		end
		dot = this:processSingle('series', false, "", "")
		if bit.band(scheme, 4) == 4 then
			this:processSet({'seriesVolume', 'seriesNumber'}, ', Band ', '.')
		elseif bit.band(scheme, 8) == 8 then
			dot = this:processSingle('seriesNumber', dot, ', Nummer ')
		elseif bit.band(scheme, 33) == 1 then
			this:processSet({'volume', 'number'}, ', Band ', '.')
		elseif bit.band(scheme, 34) == 2 then
			dot = this:processSingle('number', dot, ', Nummer ')
		end
		this.stringVal = this.stringVal .. ').'
		dot = false
	end
	fullstop()
	dot = this:processSingle('publisher', false, ' ')
	dot = this:processList('PoPub', dot, '/') or dot
	dot = this:processObject('year', false) or dot
	if this.ISBN then
		if dot then this.stringVal = this.stringVal .. ', ' end
		this.stringVal = this.stringVal .. p.ISBN({args=this})
		dot = true
	end
	this:processSet({'ISSN', 'DNB', 'ZDB', 'DOI', 'LCCN', 'OCLC', 'Lizenznummer',
		'arXiv', 'bibcode', 'JSTOR', 'PMC', 'PMID', 
		'chapter', 'page', 'column', 'position'}, dot, ', ')
	fullstop()
	this:processSet({
		'originalTitle', 'originalYear', 'originalLanguage', 'translator',
		'PoFind', 'URL', 'archive', 'fileFormat', 'dataSize', 'retrieved',
		'comment', 'dataLink'
	}, ' (', ', ', ').')
	return this.stringVal
end

--[[
	ISBN(source)
	plain value, type and formated value of an ISBN found in source
	parameters:
		source:	string containing a sequence of cyphers and hyphens
				optionally followed by a capital X
	returns:	a structure: {plain, key, formated}
		plain		all cyphers (and 'X') in direct sequence without hyphens
		key			'ISBN-10', or 'ISBN-13' (if valid)
					'ISBNinvalid' (if no ISBN)
					'ISBNwrong' (if miscalculated)
		formated	typically formated ISBN (if ISBN-10, ISBN-13, or ISBNwrong)
					'' (if ISBNinvalid)
]]
p.service.ISBN = function(source)
	local plain = source:gsub('-', ''):match('%d+X?')
	if #plain == 14 and plain:sub(14,1) == 'X' then plain = plain:sub(1,13) end
	if #plain ~= 10 and #plain ~= 13 then
		return { ["plain"] = plain, key = 'ISBNinvalid', formated = '' }
	end
	local result = { ["plain"] = plain }
	local stab = mw.text.split(plain, '')
	local checksum = 0
	if #plain == 10	then
		f = source:match('%d+%-%d+%-%d+%-[0-9X]')
		if not f then
			return { ["plain"] = plain, key = 'ISBNinvalid', formated = '' }
		end
		result.formated = f
		result.group, result.publisher, result.title
										= source:match('(%d+)%-(%d+)%-(%d+)')
		for i = 1, 9 do checksum = checksum + i * stab[i] end
		result.check = checksum % 11
		if result.check == 10 then result.check = 'X'
		else result.check = tostring(cr) end
		if result.check == stab[10] then result.key = 'ISBN-10'
		else result.key = 'ISBNwrong' end
		return result
	end
	-- must be ISBN-13
	f = source:match('97[89]%-%d+%-%d+%-%d+%-%d')
	if not f then
		return { ["plain"] = plain, key = 'ISBNinvalid', formated = '' }
	end
	result.formated = f
	result.prefix, result.group, result.publisher, result.title
									= source:match('(%d+)%-(%d+)%-(%d+)%-(%d+)')
	for i = 1, 12 do
		checksum = checksum + stab[i] + (i+1) % 2 * 2 * stab[i]
	end
	result.check = tostring(10 - checksum % 10)
	if result.check == stab[13] then result.key = 'ISBN-13'
	else result.key = 'ISBNwrong' end
	return result
end -- p.service.ISBN

--[[
	byWikiData(qualifier)
	convert wikiData item into Lua struct limited to relevant data
	mandantory data: title, year and either authors or editors
	parameter:
		qualifier:	wikidata entity qualifier string ([qQ][0-9]+) of an
					appropriate wikiData item
		noInherit:	do not use legators if set
	returns:	available data in a struct; items formated
				inavailable data is nil
				struct contains all mandantory data or is nil itself
]]
p.service.byWikiData = function(qualifier, noInherit)
	local result = p.service.dataset:new(qualifier)
	--[[
		conditionally add legator
		part of the legator are the named property itself, its qualifiers volume
		and number, as well as volume and number of the containing object
		Volume and number ...
		*	of property are ignored if given in external object already.
		*	of containing object are ignored if given in property or externally.
		This way series volume and number may differ from partOf volume and No.
	]]
	local function condAddLegator(property)
		local bsPartOf = mw.wikibase.getBestStatements(qualifier, property)
		if not bsPartOf then return end
		if not bsPartOf[1] then return end
		local bsP1 = bsPartOf[1]
		local id = bsP1.mainsnak.datavalue.value.id
		if id then result[property] = id
		else return end
		local volume = Limited.qualifyingValue(bsP1, 'P478')
		if volume == '' then
			volume = Limited.MainSnackValue(qualifier, 'P478')
		end
		local number = Limited.qualifyingValue(bsP1, 'P433')
		if number == '' then
			number = Limited.qualifyingValue(qualifier, 'P1545')
		end
		if number == '' then
			number = Limited.MainSnackValue(qualifier, 'P433')
		end
		if number == '' then
			number = Limited.MainSnackValue(qualifier, 'P1545')
		end
		if not result:bequeath(property, volume, number) then
			result[property] =  mw.wikibase.getLabel(id)
		end
	end

	-- byWikiData starting here:
	-- Author(s)
	result:addList(	transCode.tcP['P50'],
					Limited.namedAsList(qualifier, 'P50', false, 'id'))
	result:addList(	transCode.tcP['P2093'],
					Limited.namedAsList(qualifier, 'P2093'))
	result:addList(	transCode.tcP['P767'],
					Limited.namedAsList(qualifier, 'P767', false, 'id'))
	-- editor(s)
	result:addList(	transCode.tcP['P98'],
					Limited.namedAsList(qualifier, 'P98', false, 'id'))
	result:addList(	transCode.tcP['P5769'],
					Limited.namedAsList(qualifier, 'P5769'))
	-- title: mandantory; with link if available
	local p = Titles:new(qualifier)
	if not p then return nil end
	result.title = p:titleLink()
	if p.originalTitle then result.originalTitle = p.originalTitle.text end
	if p.originalLanguage then result.originalLanguage = p.originalLanguage
	else result:condAdd(transCode.tcP['P364'],
						Limited.MainSnackValue(qualifier, 'P364')) end
	-- add translator in combination with original language only
	if result.originalLanguage then
		result:condAdd(transCode.tcP['P655'],
						Limited.MainSnackValue(qualifier, 'P655')) end
	-- subtitle
	result:condAdd(	transCode.tcP['P1680'],
					Limited.MainSnackValue(qualifier, 'P1680'))
	-- edition number: preferably "object named as"
	local bsEdition = Limited.namedAsList(qualifier, 'P9767', false)
	if not bsEdition or not bsEdition[1] then
		bsEdition = Limited.namedAsList(qualifier, 'P393', false)
	end
	if bsEdition[1] then result.edition = bsEdition[1] end
	if bsEdition[2] then
		mw.log('Pls. have another entity for each edition of ' .. result.title
			.. ' (variant to ' .. qualifier .. ')')
	end
	-- publisher:
	result:condAdd(	transCode.tcP.P123,
					Limited.MainSnackValue(qualifier, 'P123'))
	-- place of publication:
	result:addList(	transCode.tcP.P291,
					Limited.namedAsList(qualifier, 'P291'))
	-- year of publication:
	result:condAdd(	transCode.tcP.P577,
					Limited.MainSnackValue(qualifier, 'P577'))
	-- language: only if not German
	result:condAdd(	transCode.tcP.P407,
					Limited.MainSnackValue(qualifier, 'P407'))
	-- URL
	local class = transCode.tcClass.URL
	mw.log(Limited.MainSnackValue(qualifier, 'P2699'))
	local object = class:new(Limited.MainSnackValue(qualifier, 'P2699'))
	mw.logObject(object, 'URL')
	result:condAdd(	transCode.tcP.P2699, object)
	mw.logObject(result, 'URL enterred')
	-- identifiers:
	for _, v in ipairs(identifiers) do
		result:condAdd(	transCode.tcP[v],
						Limited.MainSnackValue(qualifier, v))
	end
	
	-- partOf ("In") and series:
	-- fall back to data from legators if available
	if noInherit then
		result:condAdd(	transCode.tcP['P361'],
						Limited.MainSnackValue(qualifier, 'P361'))
		if not result.partOf then
			result:condAdd(	transCode.tcP['P1433'],
							Limited.MainSnackValue(qualifier, 'P1433'))
		end
		result:condAdd(	transCode.tcP['P179'],
						Limited.MainSnackValue(qualifier, 'P179'))
	else
		condAddLegator('P361')
		if not result.partOf then condAddLegator('P1433') end
		condAddLegator('P179')
	end
	return result
end

--[[
	byParamSet(source)
	convert a set of numerous parameters into Lua struct limited to relevant
	data
	parameters:
		source:	string with '|' separating between parameters and '=' separating
				between German param name as in transCode.tcGer and param value
	returns:	available data in a struct; items formated
				inavailable data is nil
				struct contains all mandantory data or is nil itself
]]
p.service.byParamSet = function(source)
	local result = p.service.dataset:new(source)
	-- legator for inherited additional infos
	local Legator = {
		prio = {title=3, partOf=2, series=1},
		level = 0,
		link = '',
		add = function(this, pName, value)
			local ppt = transCode.tcLegator[pName]
			if not ppt then return false end
			local lid = value:match('Q%d+')
			if not lid then
				return result:condAdd(pName, "''" .. value .. "''")
			end
			this[pName] = this:new{ property = ppt, ID = lid, name = pName}
			return true
		end,
		bequeath = function(this)
			local source = p.service.byWikiData(this.ID)
			if source then
				result:addSet(source, 'tc' .. this.property)
				local newLevel = this.prio[this.name]
				if newLevel > this.level then
					this.level = newLevel
					this.link = id
				end
			else
				result[this.name] = Limited.MainSnackValue(this.ID, 'P1476')
				if not result[this.name] then
					result[this.name] =  mw.wikibase.getLabel(this.ID)
				end
			end
		end,
		add2comment = function()
			if level > 0 then
				local lnk = mw.text.format(transCode.tcFormat.dataLinik, link)
				if result.comment then
					result.comment = result.comment .. ', ' .. lnk
				else
					result.comment = lnk
				end
			end
		end
	}
	function Legator:new (o)
      o = o or {}   -- create object if user does not provide one
      setmetatable(o, self)
      self.__index = self
      return o
    end
	
	-- start here:
	if type(source) ~= 'table' then return nil end
	for key, value in pairs(source) do
		local k2 = transCode.tcGer[key]
		local handling = 0
		if k2 then handling = cpHandling[k2] end
		if not handling then handling = 0 end
		if handling == 1 then
			result:condAdd(k2, value)
		elseif handling == 2 then
			result:addList(k2, value)
		elseif handling == 3 then
			--result:addLegator(k2, value)
			Legator:add(k2, value)
		elseif handling == 4 then
			result:condAdd(k2, value)
		elseif handling == 5 then
			local class = transCode.tcClass[k2]
			if class then
				local object = class:new(value)
				result:condAdd(k2, object)
			else mw.logObject(	transCode.tcClass,
								'no class tcClass.' .. k2 .. ' in') end
		end
	end
	--result:bequeath(title)
	--result:bequeath(partOf)
	--result:bequeath(series)
	if Legator.title then Legator.title:bequeath() end
	if Legator.partOf then Legator.partOf:bequeath() end
	if Legator.series then Legator.series:bequeath() end
	if result.year and not result.year.valid and result.year.valid ~= nil then
		result.year = nil
	end
	if result.ID then return result end
	-- else generate ID
	local IDP = result.authors
	if not IDP then IDP = result.editors end
	if IDP then
		local shortPs = {}
		for _, full in ipairs(IDP) do
			full = mw.ustring.gsub(full, "^%A*(.-)%A*$", "%1")
			local lw = mw.ustring.match(full, "%a+$")
			if lw  then table.insert(shortPs, lw) end
		end
		if result.year then -- ####
			result.ID = table.concat(shortPs, ', ') .. ' '
		..	result.year:format('yyyy')
		else
			result.ID = table.concat(shortPs, ', ')
		end
		return result
	end
	-- else generate by title:
	local IDT = result.partOf
	if not IDT then IDT = result.series end
	if not IDT then IDT = result.title end
	if not IDT then return nil end
	IDT = IDT:gsub("^%A*(.-)%A*$", "%1") -- trim: remove link, e.g.
	if result.year then
		if #IDT < 15 then
			result.ID = '(' .. IDT .. ') ' .. result.year:format('yyyy')
			return result
		end
	elseif #IDT < 20 then
		result.ID = '(' .. IDT .. ')'
		return result
	end
	-- if too long use last words
	local IDwords = mw.text.split(IDT, '%A+')
	local i = #IDwords
	IDT = IDwords[i]
	while i > 1 and #IDT < 7 do
		i = i - 1
		IDT = IDwords[i] .. ' ' .. IDT
	end
	if result.year then
		result.ID = '(' .. IDT .. ') ' .. result.year:format('yyyy')
	elseif i > 1 and #IDT < 10 then
		i = i - 1
		result.ID = '(' .. IDwords[i] .. ' ' .. IDT .. ')'
	else
		result.ID = '(' .. IDT .. ')'
	end

	return result
end

-------------------- template functions --------------------
-- functions to be called from within templates
------------------------------------------------------------

--[[
	Literatur|ISBN|<ISBN>
	returns formated ISBN 
	parameters:
		<unnamed> or ISBN=	string containing a sequence of cyphers and hyphens
							optionally followed by a capital X
	returns:	ISBN formated with one out of three possible formt strings in
				transCode.tcFormat: ISBN, ISBNwrong, ISBNinvalid
]]
p.ISBN = function(frame)
	local source = frame.args.ISBN
	if source == nil or source == "" then source = frame.args[1] end
	if source == nil or source == "" then return "" end
	if frame.args.plain then return source:gsub('-', ''):match('%d+X?') end
	local data = p.service.ISBN(source)
	if data.key == 'ISBNinvalid' then
		return string.format(transCode.tcFormat[data.key], source, source)
	end
	return string.format(	transCode.tcFormat[data.key],
							data.formated, data.formated)
end

--[[
	Literatur|byWikiData|qualifier=<Q...>|position=<S. ...>
	generates a cite entry
	parameters:
		qualifier (mandantory):	wikidata entity qualifier string ([qQ][0-9]+) of
								an appropriate wikiData item
		position (optional):	additional addenum for the end of the entry
								free text equivalent to position{<free text>} or
								page{<number>} column{<number>} position{<text>}
	returns:	limited literature information string
				only one (by priority) of ISBN13, ISBN10, DNB
]]
p.byWikiData = function(frame, qualifier)
	if not qualifier then qualifier = frame.args.qualifier end
	if not qualifier or qualifier == "" then qualifier = frame.args[1] end
	if not qualifier or qualifier == "" then
		return 'Fehler: kein Buch![[Kategorie:Wikipedia:Qualitätssicherung Vorlageneinbindung fehlerhaft]]'
	end
	local position = frame.args.position
	local dataSet = p.service.byWikiData(qualifier)
	if dataSet == nil then
		return 'Fehler: kein Buch![[Kategorie:Wikipedia:Qualitätssicherung Vorlageneinbindung fehlerhaft]]'
	end
	if position and position ~= "" then
		if position:match('{') then
			local p = Parser.altParse(position)
			for k, v in pairs(p) do dataSet[k] = v end
		else
			dataSet.position = position
		end
	end
	if dataSet.ID then
		local ID = dataSet.ID
		dataSet.ID = nil
		return '<span id="' .. ID .. '">' .. ID .. ' - <span class="reference-t'
			.. 'ext">' .. dataSet:toString() .. '</span></span>'
	end
	return dataSet:toString()
end

--[[
Literatur|listEntry|Titel=...
complete bibliography entry for further processing in Literatur|list
parameters
	see tcGer
returns	<ID>{entry}
]]
p.listEntry = function(frame)
	local result = p.service.byParamSet(frame:getParent().args)
	if not result then result = p.service.byParamSet(frame.args) end
	if not result then return nil end
	if not result.ID then return nil end
	local ID = result.ID
	result.ID = nil
	return ID .. '{' .. result:toString() .. '}'
end

--[[
Literatur|shortRef	|ID=<ID>|position=<reference text>|name=<ref name>
					|group=<ref group>
shortened reference entry
parameters:
	ID			see tcGer; mandantory; links to bibliography entry; no ref name
				per default (in contrast to fullRef)
	position	for what ever to be added to ref
				optional; default: "passim.";
				"p. 42.", "table 15.", "(take a note on the graphics)." e. g.
	name		*value - for <ref name="*value">
				optional; default: empty (in contrast to fullRef);
				have "name=#ID" to use ID as ref name (as in fullRef)
	group		**value - for <ref group="**value">
				optional; default: empty
]]
p.shortRef = function(frame)
	local ID = frame.args.ID
	if not ID then return '' end
	local position = frame.args.position
	if not position or position == '' then position = 'passim.' end
	local refPar = {name = frame:getParent().args.name}
	if not refPar.name then refPar.name = frame.args.name end
	refPar.group = frame:getParent().args.group
	if not refPar.group then group = frame.args.group end
	if refPar.name then
		if refPar.name == '#ID' then refPar.name = ID
		elseif refPar.name == '' then refPar.name = nil end
	end
	if refPar.group and refPar.group == '' then refPar.group = nil end
	local result = '[[#' .. ID .. '|' .. ID .. ']], ' .. position
	result = frame:extensionTag('span', result, {class='reference'})
	return frame:extensionTag('ref', result, refPar)
end

--[[
Literatur|fullRef|Titel=...
complete bibliographical reference entry
parameters
	see tcGer
	in addition:
		group	see <ref group="...">
returns	<ref>entry</ref>
]]
p.fullRef = function(frame)
	local result = p.service.byParamSet(frame:getParent().args)
	if not result then result = p.service.byParamSet(frame.args) end
	if not result then return nil end
	if not result.ID then return nil end
	local refPar = {name = frame:getParent().args.name}
	if not refPar.name then refPar.name = frame.args.name end
	if not refPar.name then refPar.name = result.ID end
	result.ID = nil
	if refPar.name and refPar.name == '' then refPar.name = nil end
	refPar.group = frame:getParent().args.group
	if not refPar.group then refPar.group = frame.args.group end
	if refPar.group and refPar.group == '' then refPar.group = nil end
	return frame:extensionTag('ref', result:toString(), refPar)
end

p.pur = function(frame)
	local result = p.service.byParamSet(frame:getParent().args)
	if not result then result = p.service.byParamSet(frame.args) end
	if not result then return nil end
	return frame:preprocess(result:toString())
end

--[[
Literatur|anchor|Titel=...
complete bibliography entry with anchor
parameters
	see tcGer
returns	<span id="<ID>"><ID> - <span class="reference-text"><entry></span></span>
]]
p.anchor = function(frame)
	local result = p.service.byParamSet(frame:getParent().args)
	if not result then result = p.service.byParamSet(frame.args) end
	if not result then return nil end
	if not result.ID then return nil end
	local ID = result.ID
	result.ID = nil
	return '<span id="' .. ID .. '">' .. ID
		.. ' - <span class="reference-text">' .. result:toString()
		.. '</span></span>'
end

--[[
Literatur|list|<bibliography entries>
parameters
	1 (unnamed) string containing aligned bibliography entries in this format:
				<key1>{<entry1>}<key2>{<entry2>}...
				these entries may be white space separated at will. e. g.:
				Tom 1811{Antony Tom: ''my first book''. hydraulic press 1811.}
				Dick 1912{Bert Dick: ''a book''. another press 1912.}
				Harry 2013{Q12345678}
	Usage of Wikidata qualifiers not yet implemented
	returns:	string containing ordered bibliography in html list form
]]
p.list = function(frame)
	local args = frame:getParent().args[1]
	if not args then args = frame.args[1] end
	if not args then return "" end
	-- collect data
	if args == "" then return "" end
	local bList = Parser.altParse(args)
	-- process data
	function compKeys(a, b)
		return a[1]<b[1]
	end
	local cList = {}
	for i, e in ipairs(bList) do
		local id = e[2]:match('^%s*[qQ]%d+%s*$')
		if id then
			local entry = p.service.byWikiData(id)
			if entry then
				if entry.ID then
					if e[1] == '' then e[1] = entry.ID end
					entry.ID = nil
				end
				e[2] = entry:toString()
				table.insert(cList, e)
			-- else don't insert
			end
		elseif e[2] ~= '' then table.insert(cList, e)
		-- else don't insert
		end
	end
	table.sort(cList, compKeys)
	local result = ""
	local keyAddendum = 0
	for i, k in ipairs(cList) do
		if k[1] == '' then
			keyAddendum = keyAddendum + 1
			result = result .. '* <span id="Lit' .. keyAddendum .. '">'
		elseif i > 1 and k[1] == bList[i-1][1] then
			keyAddendum = keyAddendum + 1
			result = result .. '* <span id="' .. k[1] .. '_' .. keyAddendum .. '">'
										  .. k[1] .. '_' .. keyAddendum .. ' – '
		else
			keyAddendum = 0
			result = result .. '* <span id="' .. k[1] .. '">' .. k[1] .. ' – '
		end
		result =	result .. '<span class="reference-text">' .. k[2]
				..	'</span></span>'
		if i < #bList then result = result .. "\n" end
	end
	-- return data
	mw.log(result .. '###')
	if result ~= "" then
		return result
	else return "" end
end

p.List2 = function(frame)
	local args = frame:getParent().args[1]
	if not args then args = frame.args[1] end
	if not args then return "" end
	-- collect data
	if args == "" then return "" end
	local bList = Parser.altParse(args)
	-- process data
	function compKeys(a, b)
		return a[1]<b[1]
	end
	for i, e in ipairs(bList) do
		local id = e[2]:match('^%s*[qQ]%d+%s*$')
		if id then
			local entry = p.service.byWikiData(id)
			if entry then
				if entry.ID then
					if e[1] == '' then bList[i][1] = entry.ID end
					entry.ID = nil
				end
				bList[i][2] = entry:toString()
			end
		end
	end
	table.sort(bList, compKeys)
	local result = ""
	local keyAddendum = 0
	for i, k in ipairs(bList) do
		if k[1] == '' then
			keyAddendum = keyAddendum + 1
			result = result .. '* <span id="Lit' .. keyAddendum .. '">'
		elseif i > 1 and k[1] == bList[i-1][1] then
			keyAddendum = keyAddendum + 1
			result = result .. '* <span id="' .. k[1] .. '_' .. keyAddendum .. '">'
										  .. k[1] .. '_' .. keyAddendum .. ' – '
		else
			keyAddendum = 0
			result = result .. '* <span id="' .. k[1] .. '">' .. k[1] .. ' – '
		end
		result =	result .. '<span class="reference-text">' .. k[2]
				..	'</span></span>'
		if i < #bList then result = result .. "\n" end
	end
	-- return data
	if result ~= "" then
		return result
	else return "" end
end

p.test = function(frame)
	local testcase = p[frame.args.func](frame)
	local e = frame.args.expected
	local i = e:find(testcase, nil, true)
	if i then
		if testcase == e then return e .. '</td></tr><tr><td>' .. e end
		if i > 1 then
			return	e .. '</td></tr><tr><td style="background-color:#f88;">'
				..	e:sub(1, i) .. ' vorweg erwartet!'
		elseif #e > #testcase then
			return	e .. '</td></tr><tr><td style="background-color:#f88;">'
				..	e:sub(#testcase) .. ' danach erwartet!'
		end
	end
	i = testcase:find(e, nil, true)
	if i then
		if i > 1 then
			return	e .. '</td></tr><tr><td style="background-color:#f88;">'
				..	testcase:sub(1, i) .. ' zu viel vorweg!'
		elseif #e < #testcase then
			return	e .. '</td></tr><tr><td style="background-color:#f88;">'
				..	testcase:sub(#e) .. ' zu viel danach!'
		end
	end
	local t1 = mw.text.split(testcase, '')
	local t2 = mw.text.split(e, '')
	mw.log(testcase)
	mw.log(e)
	for i, v in ipairs(t1) do
		if v ~= t2[i] then
			return e .. '</td></tr><tr><td style="background-color:#f88;">Fehle'
				.. 'r ab Position ' .. i .. ': ' .. v .. '/' .. t2[i]
		end
	end
	if #t1 == #t2 then return e .. '</td></tr><tr><td>' .. e end
end

return p