add analytics: mod-IoC

This commit is contained in:
relikd
2021-02-15 03:05:09 +01:00
parent 296474ca78
commit d2e5ad9b86
55 changed files with 9706 additions and 1664 deletions

View File

@@ -13,9 +13,9 @@ from LPath import FILES_ALL, FILES_UNSOLVED, LPath
#########################################
class InterruptDB(object):
def __init__(self, data, interrupt):
def __init__(self, data, interrupt, irp_stops=None):
self.irp = interrupt
self.iguess = InterruptSearch(data, irp=interrupt)
self.iguess = InterruptSearch(data, irp=interrupt, irp_stops=irp_stops)
self.irp_count = len(self.iguess.stops)
def make(self, dbname, name, keylen, fn_score):
@@ -99,21 +99,18 @@ class InterruptDB(object):
#########################################
def get_db(fname, irp, max_irp):
T = False # inverse
_, Z = InterruptIndices().consider(fname, 28 - irp if T else irp, max_irp)
stops, Z = InterruptIndices().consider(fname, irp, max_irp)
data = RuneTextFile(LPath.page(fname)).index_no_white[:Z]
if T:
data = [28 - x for x in data]
return InterruptDB(data, irp)
return InterruptDB(data, irp, irp_stops=stops)
def create_primary(dbname, fn_score, klset=range(1, 33),
max_irp=20, irpset=range(29)):
max_irp=20, irpset=range(29), files=FILES_ALL):
oldDB = InterruptDB.load(dbname)
oldValues = {k: set((a, b, c) for a, _, b, c, _ in v)
for k, v in oldDB.items()}
for irp in irpset: # interrupt rune index
for name in FILES_ALL:
for name in files:
db = get_db(name, irp, max_irp)
print('load:', name, 'interrupt:', irp, 'count:', db.irp_count)
for keylen in klset: # key length
@@ -142,9 +139,49 @@ def create_secondary(db_in, db_out, fn_score, threshold=0.75, max_irp=20):
print('found', c, 'additional solutions')
def create_mod_a_db(dbprefix, fn_score, klpairs, max_irp=20, irpset=[0, 28]):
for mod, upto in klpairs:
for mo in range(mod):
# if needed add combined check for all modulo parts
def xor_split(data, keylen):
return fn_score(data[mo::mod], keylen)
create_primary(f'db_{dbprefix}_mod_a_{mod}.{mo}', xor_split,
range(1, upto + 1), max_irp, irpset, FILES_UNSOLVED)
def create_mod_b_db(dbprefix, fn_score, klpairs, max_irp=20, irpset=[0, 28]):
db_i = InterruptIndices()
for mod, upto in klpairs:
for mo in range(mod):
dbname = f'db_{dbprefix}_mod_b_{mod}.{mo}'
oldDB = {k: set((a, b, c) for a, _, b, c, _ in v)
for k, v in InterruptDB.load(dbname).items()}
for irp in irpset: # interrupt rune index
for name in FILES_UNSOLVED:
stops, Z = db_i.consider_mod_b(name, irp, max_irp, mod)
stops = stops[mo]
Z = Z[mo]
data = RuneTextFile(LPath.page(name)).index_no_white
data = data[mo::mod][:Z]
db = InterruptDB(data, irp, irp_stops=stops)
print(f'load: {name} interrupt: {irp} count: {len(stops)}')
for keylen in range(2, upto + 1): # key length
if (db.irp_count, irp, keylen) in oldDB.get(name, []):
print(f'{keylen}: skipped.')
continue
score, irps = db.make(dbname, name, keylen, fn_score)
print(f'{keylen}: {score:.4f}, solutions: {len(irps)}')
if __name__ == '__main__':
create_primary('db_high', Probability.IC_w_keylen, max_irp=20)
create_primary('db_norm', Probability.target_diff, max_irp=20)
create_mod_a_db('high', Probability.IC_w_keylen, [(2, 13), (3, 8)])
create_mod_a_db('norm', Probability.target_diff, [(2, 13), (3, 8)])
create_mod_b_db('high', Probability.IC_w_keylen, [(2, 18), (3, 18)])
create_mod_b_db('norm', Probability.target_diff, [(2, 18), (3, 18)])
# create_secondary('db_high', 'db_high_secondary',
# Probability.IC_w_keylen, threshold=1.4)
# create_secondary('db_norm', 'db_norm_secondary',

View File

@@ -17,6 +17,20 @@ class InterruptIndices(object):
total = self.pos[name]['total'] if len(nums) <= limit else nums[limit]
return nums[:limit], total
def consider_mod_b(self, name, irp, limit, mod):
sets = [[] for _ in range(mod)]
for x in self.pos[name]['pos'][irp]:
mm = x % mod
sets[mm].append((x - mm) // mod)
tot = self.pos[name]['total']
totals = [(tot // mod)] * mod
for i, x in enumerate(sets):
if len(x) > limit:
totals[i] = x[limit]
elif i < tot % mod:
totals[i] += 1
return [x[:limit] for x in sets], totals
def total(self, name):
return self.pos[name]['total']
@@ -62,3 +76,9 @@ if __name__ == '__main__':
for name, val in InterruptIndices.load().items():
print(name, 'total:', val['total'])
print(' ', [len(x) for x in val['pos']])
print()
for mod in range(1, 4):
print(f'file: p0-2, maxirp: 20, mod: {mod}')
pos, limit = InterruptIndices().consider_mod_b('p0-2', 0, 20, mod)
for i in range(mod):
print(' ', limit[i], pos[i])

View File

@@ -9,10 +9,13 @@ import bisect # bisect_left, insort
#########################################
class InterruptSearch(object):
def __init__(self, arr, irp): # remove all whitespace in arr
def __init__(self, arr, irp, irp_stops=None): # remove whitespace in arr
self.single_result = False # if False, return list of equal likelihood
self.full = arr
self.stops = [i for i, n in enumerate(arr) if n == irp]
if irp_stops is None:
self.stops = [i for i, n in enumerate(arr) if n == irp]
else:
self.stops = irp_stops
def to_occurrence_index(self, interrupts):
return [self.stops.index(x) + 1 for x in interrupts]

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from Alphabet import RUNES
from LPath import FILES_ALL, FILES_SOLVED, LPath
from LPath import FILES_ALL, FILES_SOLVED, FILES_UNSOLVED, LPath
from InterruptDB import InterruptDB
from InterruptIndices import InterruptIndices
from RuneText import RuneTextFile
@@ -13,10 +13,79 @@ HIGH_MIN = 1.25
HIGH_MAX = 1.65
def mark(x, low=0, high=1):
if x <= low:
return ' class="m0"'
return f' class="m{int((min(high, x) - low) / (high - low) * 14) + 1}"'
class HTML(object):
@staticmethod
def attr(attrib):
txt = ''
if attrib:
for k, v in attrib.items():
txt += f' {k}="{v}"'
return txt
@staticmethod
def mark(x, low=0, high=1):
if x <= low:
return ' class="m0"'
return f' class="m{int((min(high, x) - low) / (high - low) * 14) + 1}"'
@staticmethod
def p_warn(content):
return f'<p class="red">{content}</p>\n'
@staticmethod
def dt_dd(title, content, attrib=None):
return f'<dt>{title}</dt>\n<dd{HTML.attr(attrib)}>\n{content}</dd>\n'
@staticmethod
def num_stream(stream, score_min=0, score_max=1):
dot2 = isinstance(score_min, float) or isinstance(score_max, float)
txt = ''
for x in stream:
txt += '<div'
if isinstance(x, tuple):
txt += HTML.attr(x[0])
x = x[1]
if not isinstance(x, str):
txt += HTML.mark(x, score_min, score_max)
txt += f'>{x:.2f}</div>' if dot2 else f'>{x}</div>'
return txt + '\n'
@staticmethod
def num_table(table, num_ranges, thr=1, thc=1):
txt = '<table>\n'
for r, row in enumerate(table):
attr = ''
if isinstance(row, tuple):
attr = HTML.attr(row[0])
row = row[1]
txt += f'<tr{attr}>'
for c, val in enumerate(row):
td = 'th' if r < thr or c < thc else 'td'
attr = ''
if isinstance(val, tuple):
attr = HTML.attr(val[0])
val = val[1]
isnum = False
dot2 = False
if not isinstance(val, str):
for sc_min, sc_max, L, T, R, B in num_ranges:
if c >= L and c < R and r >= T and r < B:
isnum = True
attr += HTML.mark(val, sc_min, sc_max)
dot2 = isinstance(sc_min, float) \
or isinstance(sc_max, float)
break
if isnum:
if val <= 0:
txt += f'<{td}></{td}>'
elif dot2:
txt += f'<{td}{attr}>{val:.2f}</{td}>'
else:
txt += f'<{td}{attr}>{val}</{td}>'
else:
txt += f'<{td}{attr}>{val}</{td}>'
txt += '</tr>\n'
return txt + '</table>\n'
#########################################
@@ -31,43 +100,33 @@ class InterruptToWeb(object):
def table_reliable(self):
db_indices = InterruptIndices()
trh = '<tr class="rotate"><th></th>'
trtotal = '<tr class="small"><th>Total</th>'
trd = [f'<tr><th>{x}</th>' for x in RUNES]
del_row = [True] * 29
tbl = [({'class': 'rotate'}, [''])]
tbl += [[x] for x in RUNES]
tbl += [({'class': 'small'}, ['Total'])]
for name in FILES_ALL:
if name not in self.scores:
continue
total = db_indices.total(name)
trh += f'<th><div>{name}</div></th>'
trtotal += f'<td>{total}</td>'
tbl[0][1].append(f'<div>{name}</div>')
tbl[-1][1].append(db_indices.total(name))
for i in range(29):
scrs = self.scores[name][i][1:]
if not scrs:
trd[i] += '<td></td>'
tbl[i + 1].append('')
continue
del_row[i] = False
worst_irpc = min([x[1] for x in scrs])
if worst_irpc == 0:
if max([x[1] for x in scrs]) != 0:
trd[i] += '<td>?</td>'
tbl[i + 1].append('?')
continue
_, num = db_indices.consider(name, i, worst_irpc)
trd[i] += f'<td{mark(num, 384, 812)}>{num}</td>'
trh += '</tr>\n'
trtotal += '</tr>\n'
for i in range(29):
trd[i] += '</tr>\n'
if del_row[i]:
trd[i] = ''
return f'<table>{trh}{"".join(trd)}{trtotal}</table>'
tbl[i + 1].append(num)
return HTML.num_table(tbl, [(384, 812, 1, 1, 99, len(tbl) - 1)])
def table_interrupt(self, irp, pmin, pmax):
maxkl = max(len(x[irp]) for x in self.scores.values())
trh = '<tr class="rotate"><th></th>'
trbest = '<tr class="small"><th>best</th>'
trd = [f'<tr><th>{x}</th>' for x in range(maxkl)]
tbl = [({'class': 'rotate'}, [''])]
tbl += [[x] for x in range(1, maxkl)]
tbl += [({'class': 'small'}, ['best'])]
for name in FILES_ALL:
maxscore = 0
bestkl = -1
@@ -75,21 +134,16 @@ class InterruptToWeb(object):
klarr = self.scores[name][irp]
except KeyError:
continue
trh += f'<th><div>{name}</div></th>'
tbl[0][1].append(f'<div>{name}</div>')
for kl, (score, _) in enumerate(klarr):
if score < 0:
trd[kl] += f'<td{mark(0)}></td>'
else:
trd[kl] += f'<td{mark(score, pmin, pmax)}>{score:.2f}</td>'
if kl == 0:
continue
tbl[kl].append(score)
if score > maxscore:
maxscore = score
bestkl = kl
trbest += f'<td>{bestkl}</td>'
trh += '</tr>\n'
trbest += '</tr>\n'
for i in range(29):
trd[i] += '</tr>\n'
return f'<table>{trh}{"".join(trd[1:])}{trbest}</table>'
tbl[-1][1].append(bestkl)
return HTML.num_table(tbl, [(pmin, pmax, 1, 1, 99, len(tbl) - 1)])
def make(self, outfile, pmin, pmax):
nav = ''
@@ -112,74 +166,69 @@ class ChapterToWeb(object):
def __init__(self, template='templates/pages.html'):
with open(LPath.results(template), 'r') as f:
self.template = f.read()
self.db_indices = InterruptIndices()
self.score = [(InterruptDB.load_scores('db_high'), HIGH_MIN, HIGH_MAX),
(InterruptDB.load_scores('db_norm'), NORM_MIN, NORM_MAX)]
self.db_mod = {
k: [(mod, mo, InterruptDB.load_scores(f'db_{k}_{mod}.{mo}'))
for mod in range(2, 4) for mo in range(mod)]
for k in ['high_mod_a', 'norm_mod_a', 'high_mod_b', 'norm_mod_b']
}
def pick_ngrams(self, runes, gramsize, limit=100):
res = {}
for i in range(len(runes) - gramsize + 1):
z = ''.join(x.rune for x in runes[i:i + gramsize])
ngram = ''.join(x.rune for x in runes[i:i + gramsize])
try:
res[z] += 1
res[ngram] += 1
except KeyError:
res[z] = 1
res[ngram] = 1
res = sorted(res.items(), key=lambda x: -x[1])
txt = f'<dt>{gramsize}-grams:</dt>\n'
txt += '<dd class="tabwidth">\n'
for x, y in res[:limit]:
txt += f'<div><div>{x}:</div> {y}</div>'
z = ''.join(f'<div><div>{x}:</div> {y}</div>' for x, y in res[:limit])
if len(res) > limit:
txt += f' + {len(res) - limit} others'
return txt + '</dd>\n'
z += f'+{len(res) - limit}&nbsp;others'
return HTML.dt_dd(f'{gramsize}-grams:', z, {'class': 'tabwidth'})
def sec_counts(self, words, runes):
txt = ''
txt += f'<p><b>Words:</b> {len(words)}</p>\n'
txt = f'<p><b>Words:</b> {len(words)}</p>\n'
txt += f'<p><b>Runes:</b> {len(runes)}</p>\n'
txt += '<dl>\n'
rcount = [0] * 29
for r in runes:
rcount[r.index] += 1
minmax = [min(rcount), max(rcount)]
txt += '<dt>1-grams:</dt>\n'
txt += '<dd class="tabwidth">\n'
for x, y in zip(RUNES, rcount):
txt += f'<div><div>{x}:</div> '
if y in minmax:
txt += '<b>'
txt += str(y)
if y in minmax:
txt += '</b>'
txt += '</div>'
txt += '</dd>\n'
vals = [f'<b>{y}</b>' if y in minmax else str(y) for y in rcount]
z = ''.join(f'<div><div>{x}:</div> {y}</div>'
for x, y in zip(RUNES, vals))
txt += HTML.dt_dd('1-grams:', z, {'class': 'tabwidth'})
txt += self.pick_ngrams(runes, 2, limit=100)
txt += self.pick_ngrams(runes, 3, limit=50)
txt += self.pick_ngrams(runes, 4, limit=25)
txt += '</dl>\n'
return txt
return txt + '</dl>\n'
def sec_double_rune(self, indices):
txta = '<dt>Double Runes:</dt>\n<dd class="ioc-list small one">\n'
txtb = '<dt>Rune Difference:</dt>\n<dd class="ioc-list small two">\n'
num_a = []
num_b = []
for i, (a, b) in enumerate(zip(indices, indices[1:])):
x = min(abs(a - b), min(a, b) + 29 - max(a, b))
y = 1 if x == 0 else 0
sffx = f' title="offset: {i}, rune: {RUNES[a]}"' if y else ''
txta += f'<div{mark(y)}{sffx}>{y}</div>'
txtb += f'<div{mark(x, 0, 14)} title="offset: {i}">{x}</div>'
txta += '</dd>\n'
txtb += '</dd>\n'
return '<dl>' + txta + txtb + '</dl>\n'
num_a.append(({'title': f'offset: {i}, rune: {RUNES[a]}'}, 1)
if x == 0 else '.')
num_b.append(({'title': f'offset: {i}'}, x))
txt = ''
txt += HTML.dt_dd('Double Runes:', HTML.num_stream(num_a, 0, 1),
{'class': 'ioc-list small one'})
txt += HTML.dt_dd('Rune Difference:', HTML.num_stream(num_b, 0, 14),
{'class': 'ioc-list small two'})
return '<dl>\n' + txt + '</dl>\n'
def sec_ioc(self, fname):
trh1 = '<tr><th></th>'
trh1 += '<th colspan="2">IoC-<a href="./ioc_high.html">high</a></th>'
trh1 += '<th colspan="2">IoC-<a href="./ioc_norm.html">norm</a></th>'
trh1 += '<th colspan="2">Runes / keylen</th>'
trh2 = '<tr><th></th>'
trbest = '<tr class="small"><th>best</th>'
trd = [f'<tr><th>{x}</th>' for x in range(33)]
scores = None
tbl = [['',
({'colspan': 2}, 'IoC-<a href="./ioc/high.html">high</a>'),
({'colspan': 2}, 'IoC-<a href="./ioc/norm.html">norm</a>'),
({'colspan': 2}, 'Runes / keylen')]]
tbl += [['']]
tbl += [[x] for x in range(1, 33)]
tbl += [({'class': 'small'}, ['best'])]
for scores, pmin, pmax in self.score:
for irp in [0, 28]:
maxscore = 0
@@ -188,70 +237,102 @@ class ChapterToWeb(object):
klarr = scores[fname][irp]
except KeyError:
continue
trh2 += f'<th><div>{RUNES[irp]}</div></th>'
tbl[1].append(RUNES[irp])
for kl, (score, _) in enumerate(klarr):
if score < 0:
trd[kl] += f'<td{mark(0)}></td>'
else:
trd[kl] += f'<td{mark(score, pmin, pmax)}>{score:.2f}</td>'
if kl == 0:
continue
tbl[kl + 1].append(score)
if score > maxscore:
maxscore = score
bestkl = kl
trbest += f'<td>{bestkl}</td>'
tbl[-1][1].append(bestkl)
db_indices = InterruptIndices()
for irp in [0, 28]:
try:
klarr = scores[fname][irp]
except KeyError:
continue
trh2 += f'<th><div>{RUNES[irp]}</div></th>'
tbl[1].append(RUNES[irp])
for kl, (_, maxirp) in enumerate(klarr):
if maxirp > 0:
_, num = db_indices.consider(fname, irp, maxirp)
_, num = self.db_indices.consider(fname, irp, maxirp)
num /= kl
trd[kl] += f'<td{mark(num, 29, 100)}>{int(num)}</td>'
trh1 += '</tr>\n'
trh2 += '</tr>\n'
trbest += '</tr>\n'
for i in range(len(trd)):
trd[i] += '</tr>\n'
return f'<table>{trh1}{trh2}{"".join(trd[1:])}{trbest}</table>'
tbl[kl + 1].append(int(num))
def sec_ioc_flow(self, indices, width):
txt = f'<dt>Window size {width}:</dt>\n'
txt += '<dd class="ioc-list small four">\n'
for i in range(len(indices) - width + 1):
ioc = Probability(indices[i:i + width]).IC()
clss = mark(ioc, HIGH_MIN - 0.1, HIGH_MAX)
txt += f'<div{clss} title="offset: {i}">{ioc:.2f}</div>'
txt += '</dd>\n'
return txt
return HTML.num_table(tbl, [
(HIGH_MIN, HIGH_MAX, 1, 2, 3, len(tbl) - 1),
(NORM_MIN, NORM_MAX, 3, 2, 5, len(tbl) - 1),
(28, 100, 5, 2, 7, len(tbl) - 1)
], thr=2)
def sec_ioc_mod(self, fname):
txt = '<dl>\n'
for key, minkl, maxkl in [
('high_mod_a', 1, 13), ('norm_mod_a', 1, 13),
('high_mod_b', 2, 18), ('norm_mod_b', 2, 18)
]:
tbl = [['', 'runes'] + [i for i in range(minkl, maxkl + 1)]]
type_is_mod_a = key.endswith('a')
for irp in [0, 28]:
for mod, off, scores in self.db_mod[key]:
try:
klarr = scores[fname][irp][minkl:]
maxirp = klarr[-1][1]
except (KeyError, IndexError):
continue
if type_is_mod_a:
_, num = self.db_indices.consider(fname, irp, maxirp)
num = num // mod + (1 if off < num % mod else 0)
else:
_, num = self.db_indices.consider_mod_b(
fname, irp, maxirp, mod)
num = num[off]
tr = [f'{RUNES[irp]}.{mod}.{off}', num]
tr += [score for score, _ in klarr]
tbl.append(tr)
if type_is_mod_a:
title = 'Interrupt first, then mod'
else:
title = 'Mod first, then interrupt'
if key.startswith('high'):
title += ' (IoC-<a href="../ioc/high.html">high</a>):'
tbl = HTML.num_table(tbl, [(HIGH_MIN, HIGH_MAX, 2, 1, 99, 99)])
else:
title += ' (IoC-<a href="../ioc/norm.html">norm</a>):'
tbl = HTML.num_table(tbl, [(NORM_MIN, NORM_MAX, 2, 1, 99, 99)])
txt += HTML.dt_dd(title, tbl)
return txt + '</dl>\n'
def sec_ioc_flow(self, indices):
txt = '<dl>\n'
for wsize in [120, 80, 50, 30, 20]:
nums = HTML.num_stream((({'title': f'offset: {i}'},
Probability(indices[i:i + wsize]).IC())
for i in range(len(indices) - wsize + 1)),
HIGH_MIN - 0.1, HIGH_MAX)
txt += HTML.dt_dd(f'Window size {wsize}:', nums,
{'class': 'ioc-list small four'})
return txt + '</dl>\n'
def pick_letters(self, words, idx, desc):
letters = []
for x in words:
letters.append(x[idx])
letters = [x[idx] for x in words]
ioc = Probability(x.index for x in letters).IC()
txt = f'<dt>Pick every {desc} letter (IoC: {ioc:.3f}):</dt>\n'
txt += '<dd class="runelist">\n'
for x in letters:
txt += f'<div>{x.text}</div>'
txt += '</dd>\n'
return txt
return HTML.dt_dd(f'Pick every {desc} letter (IoC: {ioc:.3f}):',
''.join(f'<div>{x.text}</div>' for x in letters),
{'class': 'runelist'})
def pick_words(self, words, n):
txt = ''
for u in range(n):
if n > 1:
txt += f'<h4>Start with {u + 1}. word</h4>\n'
subset = [x for x in words[u:None:n]]
subset = [x for x in words[u::n]]
ioc = Probability(x.index for y in subset for x in y).IC()
txt += f'<dt>Words (IoC: {ioc:.3f}):</dt>\n'
txt += '<dd>\n'
for x in subset:
txt += str(x.text) + ' '
txt += '</dd>\n'
txt += HTML.dt_dd(f'Words (IoC: {ioc:.3f}):',
''.join(x.text + ' ' for x in subset))
txt += self.pick_letters(subset, 0, 'first')
txt += self.pick_letters(subset, -1, 'last')
return txt
@@ -260,9 +341,7 @@ class ChapterToWeb(object):
txt = ''
for n in range(1, 6):
txt += f'<h3>Pick every {n}. word</h3>\n'
txt += '<dl>\n'
txt += self.pick_words(words, n)
txt += '</dl>\n'
txt += f'<dl>\n{self.pick_words(words, n)}</dl>\n'
return txt
def make(self, fname, outfile):
@@ -274,17 +353,17 @@ class ChapterToWeb(object):
html = html.replace('__SEC_COUNTS__', self.sec_counts(words, runes))
html = html.replace('__SEC_DOUBLE__', self.sec_double_rune(indices))
if fname.startswith('solved_'):
warn = '<p class="red">IoC is disabled on solved pages. Open the '
warn += f'<a href="./{fname[7:]}.html">“unsolved” page</a>'
warn += ' instead.</p>'
html = html.replace('__SEC_IOC__', warn)
ref = f'<a href="./{fname[7:]}.html">“unsolved page</a>'
html = html.replace('__SEC_IOC__', HTML.p_warn(
f'IoC is disabled on solved pages. Open the {ref} instead.'))
else:
html = html.replace('__SEC_IOC__', self.sec_ioc(fname))
ioc_flow = '<dl>\n'
for winsize in [120, 80, 50, 30, 20]:
ioc_flow += self.sec_ioc_flow(indices, winsize)
ioc_flow += '</dl>\n'
html = html.replace('__SEC_IOC_FLOW__', ioc_flow)
if fname in FILES_UNSOLVED:
html = html.replace('__SEC_IOC_MOD__', self.sec_ioc_mod(fname))
else:
html = html.replace('__SEC_IOC_MOD__', HTML.p_warn(
'Mod-IoC is disabled on solved pages'))
html = html.replace('__SEC_IOC_FLOW__', self.sec_ioc_flow(indices))
html = html.replace('__SEC_CONCEAL__', self.sec_concealment(words))
with open(LPath.results(outfile), 'w') as f:
f.write(html)
@@ -298,10 +377,8 @@ class IndexToWeb(object):
def make(self, the_links, outfile='index.html'):
html = self.template
for key, links in the_links.items():
txt = ''
for x, y in links:
txt += f' <a href="./{x}">{y}</a> '
html = html.replace(key, txt)
html = html.replace(
key, ' '.join(f'<a href="./{x}">{y}</a>' for x, y in links))
with open(LPath.results(outfile), 'w') as f:
f.write(html)