private void renderEquiv(StringBuilder sb, string equiv, CedictTargetHighlight hl, bool nobr) { TextConsumer htc = new TextConsumer(equiv, hl); bool firstWordOver = false; bool hlOn = false; char c; bool inHL; while (true) { htc.GetNext(out c, out inHL); if (c == (char)0) { break; } // Highlight starts? if (inHL && !hlOn) { // Very first word gets special highlight if hilite goes beyond first space, and we're in nobr mode if (!firstWordOver && nobr && htc.IsNextSpaceInHilite()) { sb.Append("<span class='sense-hl-start'>"); } // Plain old hilite start everywhere else else { sb.Append("<span class='sense-hl'>"); } hlOn = true; } // Highlight ends? else if (!inHL && hlOn) { sb.Append("</span>"); hlOn = false; } // Space - close "nobr" span if first word's just over if (c == ' ' && !firstWordOver && nobr) { firstWordOver = true; sb.Append("</span>"); if (hlOn) { sb.Append("</span>"); sb.Append("<span class='sense-hl-end'>"); } } // Render character sb.Append(HtmlEncoder.Default.Encode(c.ToString())); } // Close hilite and nobr that we may have open if (!firstWordOver && nobr) { sb.Append("</span>"); } if (hlOn) { sb.Append("</span>"); } }
/// <summary> /// <para>Verifies if a sense that contains all query tokens is really a match.</para> /// </summary> /// <param name="txtTokenized">The tokenized query text.</param> /// <param name="sensePos">The data position of the tokenized sense to verify.</param> /// <param name="entryIdToInfo">Container for kept entry matches.</param> /// <param name="br">Binary data source to read up tokenized sense.</param> private void doVerifyTarget(ReadOnlyCollection <EquivToken> txtTokenized, int sensePos, Dictionary <int, EntryMatchInfo> entryIdToInfo, BinReader br) { // Load tokenized sense br.Position = sensePos; TokenizedSense ts = new TokenizedSense(br); // Find query tokens in tokenized sense // This will be our highlight too! CedictTargetHighlight hilite = doFindTargetQuery(txtTokenized, ts); // No highlight: no match if (hilite == null) { return; } // Score is length of query (in tokens) divided by count of tokense in sense float score = ((float)txtTokenized.Count) / ((float)ts.EquivTokens.Count); // If we found query string, it's a match; we can go on and record best score and hilight if (!entryIdToInfo.ContainsKey(ts.EntryId)) { EntryMatchInfo emi = new EntryMatchInfo { EntryId = ts.EntryId, BestSenseScore = score, }; emi.TargetHilites.Add(hilite); entryIdToInfo[ts.EntryId] = emi; } else { EntryMatchInfo emi = entryIdToInfo[ts.EntryId]; if (score > emi.BestSenseScore) { emi.BestSenseScore = score; } emi.TargetHilites.Add(hilite); } }
public TextConsumer(string txt, CedictTargetHighlight hl) { this.txt = txt; this.hl = hl; }
private void renderSense(StringBuilder sb, CedictSense sense, ref int ix, CedictTargetHighlight hl) { bool needToSplit = true; string domain = sense.Domain; string equiv = sense.Equiv; string note = sense.Note; // Special hacks around CEDICT flat structure bool specialSense = isSpecialSense(sense); sb.Append("<span class='sense'>"); // <span class="sense"> if (!specialSense) { sb.Append("<span class='sense-nobr'>"); // <span class="sense-nobr"> sb.Append("<span class='sense-ix'>"); // <span class="sense-ix"> sb.Append(getIxString(ix)); sb.Append("</span>"); // <span class="sense-ix"> sb.Append(" "); ++ix; } if (domain != string.Empty) { sb.Append("<span class='sense-meta'>"); string[] firstAndRest = splitFirstWord(domain); sb.Append(HtmlEncoder.Default.Encode(firstAndRest[0])); sb.Append("</span>"); // sense-meta sb.Append("</span>"); // sense-nobr if (firstAndRest.Length > 1) { sb.Append("<span class='sense-meta'>"); sb.Append(HtmlEncoder.Default.Encode(firstAndRest[1])); sb.Append("</span>"); // sense-meta } needToSplit = false; } if (equiv != string.Empty) { if (specialSense) { renderSpecialSense(sb, equiv); } else { renderEquiv(sb, sense.Equiv, hl, needToSplit); } needToSplit = false; } if (note != string.Empty) { // Not since we have pre-wrap, and character-precise content in entry //if (domain != string.Empty || equiv != string.Empty) sb.Append(" "); sb.Append("<span class='sense-meta'>"); if (needToSplit) { string[] firstAndRest = splitFirstWord(note); sb.Append(HtmlEncoder.Default.Encode(firstAndRest[0])); sb.Append("</span>"); // sense-meta sb.Append("</span>"); // sense-nobr if (firstAndRest.Length > 1) { sb.Append("<span class='sense-meta'>"); sb.Append(HtmlEncoder.Default.Encode(firstAndRest[1])); sb.Append("</span>"); // sense-meta } needToSplit = false; } else { sb.Append(HtmlEncoder.Default.Encode(note)); sb.Append("</span>"); // sense-meta } } sb.Append("</span>"); // <span class="sense"> }
private void renderResult(StringBuilder sb, string lang, bool renderEntryDiv, string extraSensesClass = "") { CedictEntry entry = entryToRender; if (entry == null) { entry = res.Entry; } Dictionary <int, CedictTargetHighlight> senseHLs = new Dictionary <int, CedictTargetHighlight>(); if (res != null) { foreach (CedictTargetHighlight hl in res.TargetHilites) { senseHLs[hl.SenseIx] = hl; } } string entryClass = "entry"; if (tones == UiTones.Pleco) { entryClass += " toneColorsPleco"; } else if (tones == UiTones.Dummitt) { entryClass += " toneColorsDummitt"; } if (extraEntryClass != "") { entryClass += " " + extraEntryClass; } if (renderEntryDiv) { sb.Append("<div class='" + entryClass + "'>"); // <div class="entry"> } if (entryId != null && lang != null) { sb.Append("<a class='ajax' href='/" + lang + "/edit/existing/" + entryId + "'>"); sb.Append("<i class='fa fa-pencil entryAction edit'></i></a>"); } XRenderStatus(sb); XRenderHanzi(sb); XRenderPinyin(sb); string sensesClass = "senses"; if (!string.IsNullOrEmpty(extraSensesClass)) { sensesClass += " " + extraSensesClass; } sb.Append("<div class='" + sensesClass + "'>"); // <div class="senses"> int senseIx = 0; for (int i = 0; i != entry.SenseCount; ++i) { CedictTargetHighlight thl = null; if (senseHLs.ContainsKey(i)) { thl = senseHLs[i]; } var sense = entry.GetSenseAt(i); if (isSpecialSense(sense)) { if (i != 0) { sb.Append("<br/>"); } } else if (i != 0) { if (isSpecialSense(entry.GetSenseAt(i - 1))) { sb.Append("<br/>"); } else { sb.Append(' '); } } renderSense(sb, sense, ref senseIx, thl); } sb.Append("</div>"); // <div class="senses"> if (renderEntryDiv) { sb.Append("</div>"); // <div class="entry"> } }
private bool verifyTrg(Tokenizer tokenizer, CedictEntry entry, int entryId, int senseIx, List <Token> qtoks, List <CedictResult> res) { if (entry == null) { return(false); } // Tokenize indicated sense's equiv; see if it matches query string equiv = entry.GetSenseAt(senseIx).Equiv; List <Token> rtoks = tokenizer.Tokenize(equiv); for (int i = 0; i != rtoks.Count; ++i) { int j = 0; bool startSplit = false; bool endSplit = false; for (; j != qtoks.Count; ++j) { if (i + j >= rtoks.Count) { break; } bool ok = false; Token rtok = rtoks[i + j]; Token qtok = qtoks[j]; if (rtok.Norm == qtok.Norm) { // Stopwords: only OK if token is an entire sub-sense in retrieved sense if (!trgStopWords.Contains(rtok.Norm)) { ok = true; } else { ok = rtok.SubSeq == 0 && (i == rtoks.Count - 1 || rtoks[i + 1].SubSeq == 0); } } // First query word: can be second half of word in retrieved sense if (j == 0 && rtok.SplitPosNorm != 0) { string rTwo = rtok.Norm.Substring(rtok.SplitPosNorm); if (rTwo == qtok.Norm) { // Stopwords: only if query is min 2 tokens, or result is one token only if (!trgStopWords.Contains(rTwo) || qtoks.Count > 1) { ok = true; } startSplit = true; } } // Last query word: can be first half of word in retrieved sense if (j == qtoks.Count - 1 && rtok.SplitPosNorm != 0) { string rOne = rtok.Norm.Substring(0, rtok.SplitPosNorm); if (rOne == qtok.Norm) { // Stopwords: only if query is min 2 tokens, or result is one token only if (!trgStopWords.Contains(rOne) || qtoks.Count > 1) { ok = true; } endSplit = true; } } if (!ok) { break; } } if (j != qtoks.Count) { continue; } // We got a match starting at i! CedictTargetHighlight[] hlarr = new CedictTargetHighlight[1]; int start = rtoks[i].Start; if (startSplit) { start += rtoks[i].SplitPosSurf; } int end = rtoks[i + j - 1].Start + rtoks[i + j - 1].Surf.Length; if (endSplit) { end -= (rtoks[i + j - 1].Surf.Length - rtoks[i + j - 1].SplitPosSurf); } hlarr[0] = new CedictTargetHighlight(senseIx, start, end - start); ReadOnlyCollection <CedictTargetHighlight> hlcoll = new ReadOnlyCollection <CedictTargetHighlight>(hlarr); CedictResult cr = new CedictResult(entry, hlcoll); // Stop right here res.Add(cr); return(true); } // Not a match return(false); }
private void renderEquiv(HtmlTextWriter writer, HybridText equiv, CedictTargetHighlight hl, bool nobr) { HybridTextConsumer htc = new HybridTextConsumer(equiv, hl); bool firstWordOver = false; bool hlOn = false; char c; bool inHL; while (true) { htc.GetNext(out c, out inHL); if (c == (char)0) { break; } // Highlight starts? if (inHL && !hlOn) { // Very first word gets special highlight if hilite goes beyond first space, and we're in nobr mode if (!firstWordOver && nobr && htc.IsNextSpaceInHilite()) { writer.AddAttribute(HtmlTextWriterAttribute.Class, "sense-hl-start"); writer.RenderBeginTag(HtmlTextWriterTag.Span); } // Plain old hilite start everywhere else else { writer.AddAttribute(HtmlTextWriterAttribute.Class, "sense-hl"); writer.RenderBeginTag(HtmlTextWriterTag.Span); } hlOn = true; } // Highlight ends? else if (!inHL && hlOn) { writer.RenderEndTag(); hlOn = false; } // Space - close "nobr" span if first word's just over if (c == ' ' && !firstWordOver && nobr) { firstWordOver = true; writer.RenderEndTag(); if (hlOn) { writer.RenderEndTag(); writer.AddAttribute(HtmlTextWriterAttribute.Class, "sense-hl-end"); writer.RenderBeginTag(HtmlTextWriterTag.Span); } } // Render character writer.WriteEncodedText(c.ToString()); } // Close hilite and nobr that we may have open if (!firstWordOver && nobr) { writer.RenderEndTag(); } if (hlOn) { writer.RenderEndTag(); } }
public HybridTextConsumer(HybridText txt, CedictTargetHighlight hl) { this.txt = txt; this.hl = hl; runTxt = txt.GetRunAt(0).GetPlainText(); }
private void renderSense(HtmlTextWriter writer, CedictSense sense, int ix, CedictTargetHighlight hl) { writer.AddAttribute(HtmlTextWriterAttribute.Class, "sense"); writer.RenderBeginTag(HtmlTextWriterTag.Span); // <span class="sense"> writer.AddAttribute(HtmlTextWriterAttribute.Class, "sense-nobr"); writer.RenderBeginTag(HtmlTextWriterTag.Span); // <span class="sense-nobr"> writer.AddAttribute(HtmlTextWriterAttribute.Class, "sense-ix"); writer.RenderBeginTag(HtmlTextWriterTag.Span); // <span class="sense-ix"> writer.WriteEncodedText(getIxString(ix)); writer.RenderEndTag(); // <span class="sense-ix"> writer.WriteEncodedText(" "); bool needToSplit = true; string domain = sense.Domain.GetPlainText(); string equiv = sense.Equiv.GetPlainText(); string note = sense.Note.GetPlainText(); if (domain != string.Empty) { writer.AddAttribute(HtmlTextWriterAttribute.Class, "sense-meta"); writer.RenderBeginTag(HtmlTextWriterTag.Span); string[] firstAndRest = splitFirstWord(domain); writer.WriteEncodedText(firstAndRest[0]); writer.RenderEndTag(); // sense-meta writer.RenderEndTag(); // sense-nobr if (firstAndRest.Length > 1) { writer.AddAttribute(HtmlTextWriterAttribute.Class, "sense-meta"); writer.RenderBeginTag(HtmlTextWriterTag.Span); writer.WriteEncodedText(firstAndRest[1]); writer.RenderEndTag(); // sense-meta } needToSplit = false; } if (equiv != string.Empty) { if (domain != string.Empty) { writer.WriteEncodedText(" "); } renderEquiv(writer, sense.Equiv, hl, needToSplit); needToSplit = false; } if (note != string.Empty) { if (domain != string.Empty || equiv != string.Empty) { writer.WriteEncodedText(" "); } writer.AddAttribute(HtmlTextWriterAttribute.Class, "sense-meta"); writer.RenderBeginTag(HtmlTextWriterTag.Span); if (needToSplit) { string[] firstAndRest = splitFirstWord(note); writer.WriteEncodedText(firstAndRest[0]); writer.RenderEndTag(); // sense-meta writer.RenderEndTag(); // sense-nobr if (firstAndRest.Length > 1) { writer.AddAttribute(HtmlTextWriterAttribute.Class, "sense-meta"); writer.RenderBeginTag(HtmlTextWriterTag.Span); writer.WriteEncodedText(firstAndRest[1]); writer.RenderEndTag(); // sense-meta } needToSplit = false; } else { writer.WriteEncodedText(note); writer.RenderEndTag(); // sense-meta } } writer.RenderEndTag(); // <span class="sense"> }
private void renderResult(HtmlTextWriter writer) { int hanziLimit = isMobile ? 4 : 6; CedictEntry entry = prov.GetEntry(res.EntryId); Dictionary <int, CedictTargetHighlight> senseHLs = new Dictionary <int, CedictTargetHighlight>(); foreach (CedictTargetHighlight hl in res.TargetHilites) { senseHLs[hl.SenseIx] = hl; } string entryClass = "entry"; if (tones == UiTones.Pleco) { entryClass += " toneColorsPleco"; } else if (tones == UiTones.Dummitt) { entryClass += " toneColorsDummitt"; } writer.AddAttribute(HtmlTextWriterAttribute.Class, entryClass); writer.RenderBeginTag(HtmlTextWriterTag.Div); // <div class="entry"> if (script != UiScript.Trad) { writer.AddAttribute(HtmlTextWriterAttribute.Class, "hw-simp"); writer.RenderBeginTag(HtmlTextWriterTag.Span); // <span class="hw-simp"> renderHanzi(entry, true, false, writer); writer.RenderEndTag(); // <span class="hw-simp"> } if (script == UiScript.Both) { // Up to 6 hanzi: on a single line if (entry.ChSimpl.Length <= hanziLimit) { string clsSep = "hw-sep"; if (tones != UiTones.None) { clsSep = "hw-sep faint"; } writer.AddAttribute(HtmlTextWriterAttribute.Class, clsSep); writer.RenderBeginTag(HtmlTextWriterTag.Span); // <span class="hw-sep"> writer.WriteEncodedText("•"); writer.RenderEndTag(); // <span class="hw-sep"> } // Otherwise, line break else { writer.RenderBeginTag(HtmlTextWriterTag.Br); writer.RenderEndTag(); } } if (script != UiScript.Simp) { string clsTrad = "hw-trad"; // Need special class so traditional floats left after line break if (script == UiScript.Both && entry.ChSimpl.Length > hanziLimit) { clsTrad = "hw-trad break"; } writer.AddAttribute(HtmlTextWriterAttribute.Class, clsTrad); writer.RenderBeginTag(HtmlTextWriterTag.Span); // <span class="hw-trad"> renderHanzi(entry, false, script == UiScript.Both, writer); writer.RenderEndTag(); // <span class="hw-trad"> } writer.AddAttribute(HtmlTextWriterAttribute.Class, "hw-pinyin"); writer.RenderBeginTag(HtmlTextWriterTag.Span); // <span class="hw-pinyin"> bool firstSyll = true; foreach (var pinyin in entry.Pinyin) { if (!firstSyll) { writer.WriteEncodedText(" "); } firstSyll = false; writer.WriteEncodedText(pinyin.GetDisplayString(true)); } writer.RenderEndTag(); // <span class="hw-pinyin"> writer.AddAttribute(HtmlTextWriterAttribute.Class, "senses"); writer.RenderBeginTag(HtmlTextWriterTag.Div); // <div class="senses"> for (int i = 0; i != entry.SenseCount; ++i) { CedictTargetHighlight thl = null; if (senseHLs.ContainsKey(i)) { thl = senseHLs[i]; } renderSense(writer, entry.GetSenseAt(i), i, thl); } writer.RenderEndTag(); // <div class="senses"> writer.RenderEndTag(); // <div class="entry"> }
/// <summary> /// Breaks down body content into typographic blocks and caches the size of these. /// </summary> /// <param name="g">A Graphics object used for measurements.</param> private void doMeasureBlocks(Graphics g) { // Once measured, blocks don't change. Nothing to do then. if (measuredBlocks != null) { return; } // This is how we measure StringFormat sf = StringFormat.GenericTypographic; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; // Decide about size of sense ID up front: that's always a square, letter-height SizeF xSize = g.MeasureString("x", getFont(fntSenseLatin), 65535, sf); ushort senseIdxWidth = (ushort)Math.Ceiling(xSize.Height); // Create array with as many items as senses // Each item is null, or highlight in sense's equiv CedictTargetHighlight[] hlArr = new CedictTargetHighlight[entry.SenseCount]; foreach (CedictTargetHighlight hl in res.TargetHilites) { hlArr[hl.SenseIx] = hl; } // Recreate list of blocks List <Block> newBlocks = new List <Block>(); // Collect links here. Will only keep at end if not empty. List <LinkArea> newLinks = new List <LinkArea>(); int senseIdx = -1; int displaySenseIdx = -1; bool lastWasClassifier = false; foreach (CedictSense cm in entry.Senses) { ++senseIdx; // Is this sense a classifier? bool classifier = cm.Domain.EqualsPlainText("CL:"); if (!classifier) { ++displaySenseIdx; } // Add one block for sense ID, unless this is a classifier "sense" if (!classifier) { Block sidBlock = new Block { Width = senseIdxWidth, StickRight = true, TextPos = textPool.PoolString(getSenseIdString(displaySenseIdx)), NewLine = lastWasClassifier, SenseId = true, FirstInCedictSense = true, }; newBlocks.Add(sidBlock); } // Split domain, equiv and note into typographic parts // Splits along spaces and dashes // Unpacks Chinese ranges // Domain is localized text for "Classifier:" if, well, this is a classifier sense int startIX = newBlocks.Count; if (!classifier) { makeBlocks(cm.Domain, true, null, newBlocks, newLinks); } else { string strClassifier = tprov.GetString("ResultCtrlClassifier"); HybridText htClassifier = new HybridText(strClassifier); int ix = newBlocks.Count; makeBlocks(htClassifier, true, null, newBlocks, newLinks); Block xb = newBlocks[ix]; xb.NewLine = true; newBlocks[ix] = xb; } makeBlocks(cm.Equiv, false, hlArr[senseIdx], newBlocks, newLinks); makeBlocks(cm.Note, true, null, newBlocks, newLinks); // If sense is a classifier, mark first block as sense starter if (classifier) { Block sstart = newBlocks[startIX]; sstart.FirstInCedictSense = true; newBlocks[startIX] = sstart; } // Measure each block for (int i = startIX; i != newBlocks.Count; ++i) { Block tb = newBlocks[i]; bool isHanzi = !(tb.FontIdx == fntMetaLatin || tb.FontIdx == fntSenseLatin); SizeF sz; if (!isHanzi) { sz = g.MeasureString(textPool.GetString(tb.TextPos), getFont(tb.FontIdx), 65535, sf); } else { sz = HanziRenderer.MeasureString(g, Magic.ZhoContentFontFamily, textPool.GetString(tb.TextPos), Magic.LemmaHanziFontSize); } tb.Width = (ushort)Math.Round(sz.Width); newBlocks[i] = tb; } lastWasClassifier = classifier; } if (newLinks.Count != 0) { targetLinks = newLinks; } measuredBlocks = newBlocks.ToArray(); }
/// <summary> /// <para>Produces unmeasured display blocks from a single hybrid text. Marks highlights, if any.</para> /// <para>Does not fill in blocks' size, but fills in everything else.</para> /// </summary> /// <param name="htxt">Hybrid text to break down into blocks and measure.</param> /// <param name="isMeta">True if this is a domain or note (displayed in italics).</param> /// <param name="hl">Highlight to show in hybrid text, or null.</param> /// <param name="blocks">List of blocks to append to.</param> /// <param name="links">List to gather links (appending to list).</param> private void makeBlocks(HybridText htxt, bool isMeta, CedictTargetHighlight hl, List <Block> blocks, List <LinkArea> links) { byte fntIdxLatin = isMeta ? fntMetaLatin : fntSenseLatin; byte fntIdxZhoSimp = isMeta ? fntMetaHanziSimp : fntSenseHanziSimp; byte fntIdxZhoTrad = isMeta ? fntMetaHanziTrad : fntSenseHanziTrad; // Go run by run for (int runIX = 0; runIX != htxt.RunCount; ++runIX) { TextRun run = htxt.GetRunAt(runIX); // Latin run: split by spaces first if (run is TextRunLatin) { string[] bySpaces = run.GetPlainText().Split(new char[] { ' ' }); // Each word: also by dash int latnPos = 0; foreach (string str in bySpaces) { string[] byDashes = splitByDash(str); // Add block for each int ofsPos = 0; foreach (string blockStr in byDashes) { Block tb = new Block { TextPos = textPool.PoolString(blockStr), FontIdx = fntIdxLatin, SpaceAfter = false, // will set this true for last block in "byDashes" }; // Does block's text intersect with highlight? if (hl != null && hl.RunIx == runIX) { int blockStart = latnPos + ofsPos; int blockEnd = blockStart + blockStr.Length; if (blockStart >= hl.HiliteStart && blockStart < hl.HiliteStart + hl.HiliteLength) { tb.Hilite = true; } else if (blockEnd > hl.HiliteStart && blockEnd <= hl.HiliteStart + hl.HiliteLength) { tb.Hilite = true; } else if (blockStart < hl.HiliteStart && blockEnd >= hl.HiliteStart + hl.HiliteLength) { tb.Hilite = true; } } blocks.Add(tb); // Keep track of position for highlight ofsPos += blockStr.Length; } // Make sure last one is followed by space Block xb = blocks[blocks.Count - 1]; xb.SpaceAfter = true; blocks[blocks.Count - 1] = xb; // Keep track of position in text - for highlights latnPos += str.Length + 1; } } // Chinese: depends on T/S/Both display mode, and on available info else { TextRunZho zhoRun = run as TextRunZho; // Chinese range is made up of: // Simplified (empty string if only traditional requested) // Separator (if both simplified and traditional are requested) // Traditional (empty string if only simplified requested) // Pinyin with accents as tone marks, in brackets (if present) string strSimp = string.Empty; if (analyzedScript != SearchScript.Traditional && zhoRun.Simp != null) { strSimp = zhoRun.Simp; } string strTrad = string.Empty; if (analyzedScript != SearchScript.Simplified && zhoRun.Trad != null) { strTrad = zhoRun.Trad; } string strPy = string.Empty; // Convert pinyin to display format (tone marks as diacritics; r5 glued) if (zhoRun.Pinyin != null) { strPy = "[" + zhoRun.GetPinyinInOne(true) + "]"; } // Create link area, with query string string strPyNumbers = string.Empty; // Pinyin with numbers as tone marks if (zhoRun.Pinyin != null) { strPyNumbers = zhoRun.GetPinyinRaw(); } LinkArea linkArea = new LinkArea(strSimp, strTrad, strPyNumbers, analyzedScript); // Block for simplified, if present if (strSimp != string.Empty) { Block tb = new Block { TextPos = textPool.PoolString(strSimp), FontIdx = fntIdxZhoSimp, SpaceAfter = true, }; blocks.Add(tb); linkArea.BlockIds.Add(blocks.Count - 1); } // Separator if both simplified and traditional are there // AND they are different... if (strSimp != string.Empty && strTrad != string.Empty && strSimp != strTrad) { Block xb = blocks[blocks.Count - 1]; xb.StickRight = true; blocks[blocks.Count - 1] = xb; Block tb = new Block { TextPos = textPool.PoolString("•"), FontIdx = fntIdxLatin, SpaceAfter = true, }; blocks.Add(tb); linkArea.BlockIds.Add(blocks.Count - 1); } // Traditional, if present if (strTrad != string.Empty && strTrad != strSimp) { Block tb = new Block { TextPos = textPool.PoolString(strTrad), FontIdx = fntIdxZhoTrad, SpaceAfter = true, }; blocks.Add(tb); linkArea.BlockIds.Add(blocks.Count - 1); } // Pinyin, if present if (strPy != string.Empty) { // Split by spaces string[] pyParts = strPy.Split(new char[] { ' ' }); foreach (string pyPart in pyParts) { Block tb = new Block { TextPos = textPool.PoolString(pyPart), FontIdx = fntIdxLatin, SpaceAfter = true, }; blocks.Add(tb); linkArea.BlockIds.Add(blocks.Count - 1); } } // Last part will have requested a space after. // Look ahead and if next text run is Latin and starts with punctuation, make it stick TextRunLatin nextLatinRun = null; if (runIX + 1 < htxt.RunCount) { nextLatinRun = htxt.GetRunAt(runIX + 1) as TextRunLatin; } if (nextLatinRun != null && char.IsPunctuation(nextLatinRun.GetPlainText()[0])) { Block xb = blocks[blocks.Count - 1]; xb.SpaceAfter = false; blocks[blocks.Count - 1] = xb; } // Collect link area links.Add(linkArea); } } }