Exemple #1
0
        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>");
            }
        }
Exemple #2
0
        /// <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);
            }
        }
Exemple #3
0
 public TextConsumer(string txt, CedictTargetHighlight hl)
 {
     this.txt = txt;
     this.hl  = hl;
 }
Exemple #4
0
        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">
        }
Exemple #5
0
        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">
            }
        }
Exemple #6
0
        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);
        }
Exemple #7
0
        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();
            }
        }
Exemple #8
0
 public HybridTextConsumer(HybridText txt, CedictTargetHighlight hl)
 {
     this.txt = txt;
     this.hl  = hl;
     runTxt   = txt.GetRunAt(0).GetPlainText();
 }
Exemple #9
0
        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">
        }
Exemple #10
0
        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);
                }
            }
        }