1// Package textlab runs every string algorithm in p/moul over one input at2// once, so they can be compared rather than read about one at a time.3//4// Each of these has its own demo realm showing it alone. What none of those can5// show is how the answers differ for the SAME word, which is the only question6// a reader actually has: soundex and levenshtein both claim to tell you whether7// two words are alike, and they are measuring different things.8//9// soundex keys on the FIRST LETTER and then on consonant classes, so it is10// blind to how the rest is spelled. levenshtein counts edits and is blind to11// how it sounds. "knight" and "night" are the clean case: one edit apart (83%12// similar) and yet soundex calls them unrelated, K523 against N230, because13// the silent K changes the first character of the code. "robert" and "rupert"14// go the other way, identical codes at two edits apart. Measured 2026-09-2315// against the packages in this tree, not quoted from a textbook.16//17// /r/moul/x/allinone/textlab/v0:knight18// /r/moul/x/allinone/textlab/v0:knight/night compares two19//20// Eight packages, no state, nothing to redeploy carefully: the input comes out21// of the render path and nothing is stored.22package textlab2324import (25 "strconv"26 "strings"2728 "gno.land/p/moul/kit/ui/v0"29 "gno.land/p/moul/md/v0"30 "gno.land/p/moul/realmpath/v0"31 "gno.land/p/moul/x/daily/levenshtein/v0"32 "gno.land/p/moul/x/daily/piglatin/v0"33 "gno.land/p/moul/x/daily/romannum/v0"34 "gno.land/p/moul/x/daily/rot13/v0"35 "gno.land/p/moul/x/daily/soundex/v0"36 "gno.land/r/moul/config/v1"37)3839const realmPath = "gno.land/r/moul/x/allinone/textlab/v0"4041// maxInput bounds what a render path can carry into the algorithms below.42// levenshtein.Matrix is O(len(a) * len(b)), so an unbounded pair of inputs is43// a way to make a query expensive for whoever serves it.44const maxInput = 644546// defaultWord is what the root view demonstrates, chosen because it is the47// textbook case where the two similarity measures disagree: "knight" and48// "night" sound identical to soundex and are one edit apart.49const defaultWord = "knight"5051// Render takes its input from the path: one word, or two separated by "/".52func Render(path string) string {53 req := realmpath.Parse(path)5455 a := strings.TrimSpace(req.PathPart(0))56 b := strings.TrimSpace(req.PathPart(1))57 if a == "" {58 a = defaultWord59 }6061 var out strings.Builder62 out.WriteString(config.TopBlockFor(realmPath))63 out.WriteString(md.H1("allinone: textlab"))64 out.WriteString("\nEight packages over one input. ")65 out.WriteString(md.Link("source", config.MygnoscanFor(realmPath)))66 out.WriteString("\n\n")6768 if tooLong(a) || tooLong(b) {69 out.WriteString(ui.Empty("input too long: max " + strconv.Itoa(maxInput) + " bytes"))70 out.WriteString("\n")71 out.WriteString(config.BottomBlockFor(realmPath))72 return out.String()73 }7475 out.WriteString(renderOne(a))76 if b != "" {77 out.WriteString("\n")78 out.WriteString(renderPair(a, b))79 } else {80 out.WriteString("\n")81 out.WriteString(renderTry())82 }8384 out.WriteString(config.BottomBlockFor(realmPath))85 return out.String()86}8788func tooLong(s string) bool { return len(s) > maxInput }8990// renderOne is every single-input algorithm, one row each.91//92// ui.Cell, not ui.Inline: these land in table cells, where an unescaped pipe93// would silently eat a column. The input is whatever someone put in a URL.94func renderOne(word string) string {95 t := ui.NewTable("package", "answer")96 t.Row("`x/daily/soundex`", ui.Cell(soundex.Encode(word)))97 t.Row("`x/daily/rot13`", ui.Cell(rot13.Rot13(word)))98 t.Row("`x/daily/piglatin`", ui.Cell(piglatin.Translate(word)))99 t.Row("`x/daily/romannum`", ui.Cell(romanOf(word)))100101 return md.H2("Input: "+ui.Inline(word)) + "\n" + t.String()102}103104// romanOf shows the round trip both ways, because romannum is the one package105// here whose input is a number rather than a word.106//107// FromRoman PANICS on anything that is not a roman numeral rather than108// returning zero, and the input here comes out of a URL, so it is screened109// first. Without that screen the DEFAULT view aborts: "knight" is not a roman110// numeral, and neither is most of what anyone would type.111func romanOf(word string) string {112 if n, err := strconv.Atoi(word); err == nil {113 if n <= 0 || n > 3999 {114 return "out of range (1..3999)"115 }116 return romannum.ToRoman(n)117 }118 upper := strings.ToUpper(word)119 if !isRomanNumeral(upper) {120 return "not a number"121 }122 if n := romannum.FromRoman(upper); n > 0 {123 return strconv.Itoa(n) + " (read as roman)"124 }125 return "not a number"126}127128// isRomanNumeral reports whether every byte is a roman digit. It is a screen129// for romanOf, not a validity check: "IIII" passes here and FromRoman decides.130func isRomanNumeral(s string) bool {131 if s == "" {132 return false133 }134 for i := 0; i < len(s); i++ {135 switch s[i] {136 case 'I', 'V', 'X', 'L', 'C', 'D', 'M':137 default:138 return false139 }140 }141 return true142}143144// renderPair is the comparison, and the reason this realm exists: the two145// similarity measures answer different questions, and a reader should watch146// them disagree rather than be told they might.147func renderPair(a, b string) string {148 sa, sb := soundex.Encode(a), soundex.Encode(b)149150 t := ui.NewTable("measure", "answer")151 t.Row("`soundex` of "+ui.Cell(a), ui.Cell(sa))152 t.Row("`soundex` of "+ui.Cell(b), ui.Cell(sb))153 t.Row("sound alike", yesNo(soundex.Match(a, b)))154 t.Row("`levenshtein` distance", strconv.Itoa(levenshtein.Distance(a, b)))155 t.Row("`levenshtein` similarity", strconv.Itoa(levenshtein.Similarity(a, b))+"%")156157 return md.H2("Compared with "+ui.Inline(b)) + "\n" + t.String()158}159160func yesNo(b bool) string {161 if b {162 return "yes"163 }164 return "no"165}166167func renderTry() string {168 return md.H2("Try two") + "\n" +169 md.BulletList([]string{170 md.Link("knight vs night", "/r/moul/x/allinone/textlab/v0:knight/night") +171 " (one edit apart, yet soundex says unrelated)",172 md.Link("robert vs rupert", "/r/moul/x/allinone/textlab/v0:robert/rupert") +173 " (two edits apart, yet soundex says identical)",174 md.Link("1987 as a roman numeral", "/r/moul/x/allinone/textlab/v0:1987"),175 })176}177Render(path string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.