इस पेज का अनुवाद मशीन द्वारा किया गया है और इसमें त्रुटियाँ हो सकती हैं। मूल अंग्रेज़ी पढ़ें

एलिमेंट

TextView

TextView Markdown या HTML में लिखा हुआ, या ऐप द्वारा बनाए गए ब्लॉक के रूप में दिया गया दस्तावेज़ दिखाता है: हेडिंग, पैराग्राफ़, इमफ़ेसिस, लिंक, सूचियाँ और टास्क सूचियाँ, कोट और GitHub के अलर्ट, कोड, टेबल, इमेज, फ़ॉर्मूले, फ़ुटनोट और डिस्क्लोज़र। इसे चैट में AI मॉडल के उत्तरों के लिए बनाया गया है: उत्तर Markdown के रूप में स्ट्रीम होकर आता है, और हर चंक टेक्स्ट को उसकी फ़ॉर्मैटिंग के साथ दिखाता है। पार्सिंग Rust में होती है, और दस्तावेज़ Text एलिमेंट और व्यू से बनाया जाता है।

text-view.tsx
text-view.tsxLive
import { cleanup, mount, signal, Scroll, Text, View } from "nsh";
import { TextView } from "nsh/base";
import { button, buttonText, desktop, ui } from "./palette";

const REPLY = `## Moving to offline sync

Sync now runs **in the background**, and conflicts merge *per field*:

1. Export with \`nsh export\`
2. Check the dry run
3. Switch over

| Step   | Time  |
|--------|------:|
| Export | 2 min |
| Check  | 5 min |

\`\`\`sh
nsh export --out backup.json
\`\`\`

> [!TIP]
> ${desktop ? "Drag across the paragraphs to select them, then copy." : "Long-press a paragraph to copy it."}`;

function App() {
  const text = signal(REPLY);
  const streaming = signal(false);
  let timer: number | undefined;
  const stop = () => {
    if (timer !== undefined) clearInterval(timer);
    timer = undefined;
    streaming.set(false);
  };
  cleanup(stop);
  // Replays the reply a few characters at a time, as a model streams it.
  const play = () => {
    stop();
    const tokens = REPLY.match(/\s*\S{1,4}|\s+/g) ?? [];
    let i = 0;
    text.set("");
    streaming.set(true);
    timer = setInterval(() => (i < tokens.length ? text.set(text() + tokens[i++]) : stop()), 40);
  };
  return (
    <Scroll style={{ flex: 1, backgroundColor: ui().page }}>
      <View style={{ width: "100%", maxWidth: 680, alignSelf: "center", padding: 20, gap: 16 }}>
        <TextView
          markdown={text()}
          options={{ streaming: streaming() }}
          streamingCaret={streaming() ? <View style={{ width: 8, height: 16, marginLeft: 2, borderRadius: 2, backgroundColor: ui().accent }} /> : undefined}
          selectable
        />
        <View
          onPress={play}
          disabled={streaming()}
          {...button()}
        >
          <Text style={buttonText()}>{streaming() ? "Streaming…" : "Stream it again"}</Text>
        </View>
      </View>
    </Scroll>
  );
}

mount(() => <App />);

यह व्यवहार खुद संभालता है और पढ़ने के लिए अपनी टाइपोग्राफ़ी के साथ आता है: 15pt का टेक्स्ट 25.5pt की खुली पंक्तियों में, ऊपर जगह के साथ सेमीबोल्ड हेडिंग, थीम के primary रंग में रेखांकित लिंक, थीम के font-mono में कोड, बॉर्डर वाले गोल कोनों के बॉक्स में, हर ओर रेखाओं वाली टेबल जिसकी हेडर पंक्ति पर हल्का रंग होता है, किनारे पर रेखा वाले कोट, और GitHub के अलर्ट रंगों में आइकन वाले रंगीन बॉक्स के रूप में अलर्ट। इसके रंग थीम के टोकन हैं (foreground, muted-foreground, border, muted, primary, और सावधानी के लिए destructive), इसलिए थीम और डार्क अपीयरेंस इस तक पहुँचते हैं। हर भाग इन डिफ़ॉल्ट स्टाइल के ऊपर ऐप की स्टाइल लेता है: TSX में classes और styles, Rust में भाग के अनुसार टाइप्ड स्टाइल (TextViewParts)। Rust में इसे nsh_base::text_view() से बनाया जाता है, TSX में nsh/base से <TextView> के साथ।

use nsh_base::{TextViewLinkPressEvent, text_view, theme};
use nsh_core::prelude::*;

const README: &str = "# Release notes

**Faster lists** and a new `TextView`:

- Markdown and HTML
- [x] Streaming replies
- [ ] Your own syntax

> [!TIP]
> Style every part with `TextViewParts`.";

fn release_notes() -> impl IntoElement {
    scroll().flex_1().child(
        text_view()
            .markdown(README)
            .on_link_press(|TextViewLinkPressEvent { url, .. }: &TextViewLinkPressEvent| log::info!("open {url}"))
            .p_5()
            .text_base()
            .text_color(theme().foreground),
    )
}

Rust में text_view() इसे वैसे ही बनाता है जैसे view() व्यू बनाता है: हर प्रॉप के लिए एक सेटर (.markdown(..), .options(..), .selectable(..)), हर रेंडर फ़ंक्शन के लिए एक क्लोज़र (.render_image(|image| ..)), हर इवेंट के लिए एक लिसनर (.on_link_press(..)) और इसके रूट के लिए टाइप्ड स्टाइल मेथड (.p_5(), .text_color(..)), जिनकी टेक्स्ट स्टाइल हर भाग तक पहुँचती हैं।

#Text पर बना

TextView Text एलिमेंट से बना है, और इसका टेक्स्ट ठीक वैसे ही काम करता है जैसा Text पेज बताता है। हर पैराग्राफ़, हेडिंग, टेबल सेल और कोड ब्लॉक एक नेटिव Text है: एक ब्लॉक जो लेआउट होता है, रैप होता है, फ़ॉन्ट स्केल का पालन करता है और selectable के साथ चुना और कॉपी किया जा सकता है। स्ट्रॉन्ग, इमफ़ेसिस, स्ट्राइक-थ्रू और अन्य इनलाइन मार्क इसके स्पैन हैं; इनलाइन कोड स्पैन बैकग्राउंड वाला स्पैन है, इसलिए code: "rounded bg-neutral-100" उसे गोल बैकग्राउंड देता है; लिंक प्रेस किए जा सकने वाले स्पैन हैं; इमेज और इनलाइन कस्टम नोड इनलाइन अटैचमेंट हैं। रूट पर दी गई टेक्स्ट स्टाइल हर भाग तक पहुँचती हैं, जैसे टेक्स्ट स्टाइल एक Text से उसके स्पैन तक पहुँचती हैं।

selectable के साथ पूरा दस्तावेज़ वेब पेज की तरह एक साथ चुना जाता है: ड्रैग (या टच स्क्रीन पर लॉन्ग प्रेस और हैंडल) एक पैराग्राफ़, सूची आइटम, सेल या कोड ब्लॉक से अगले तक जाता है, और Copy उन सबका टेक्स्ट लेता है। selectable TextView अपने आप में अलग से चुना जाता है; selectable view के अंदर उसका चयन आसपास के टेक्स्ट और अन्य TextView तक भी बढ़ता है।

TextView बाकी सब जोड़ता है: Markdown, HTML या दस्तावेज़ को ब्लॉक में पढ़ना, टेक्स्ट के आसपास के व्यू (सूचियाँ, कोट, टेबल, कोड ब्लॉक, डिस्क्लोज़र, फ़ुटनोट), स्ट्रीमिंग, भाग के अनुसार स्टाइल, रेंडर फ़ंक्शन और एक्सटेंशन।

#दस्तावेज़

सामग्री तीन प्रॉप में से एक से आती है, जिन्हें इसी क्रम में पढ़ा जाता है:

  • markdown: GitHub के जोड़ों (टेबल, टास्क सूचियाँ, स्ट्राइकथ्रू, ऑटोलिंक, फ़ुटनोट, अलर्ट) के साथ CommonMark, फ़ॉर्मूले ($…$ और $$…$$) और Markdown के अंदर HTML।
  • html: HTML जैसा रीडर उसे दिखाता है। टेक्स्ट, संरचना और इनलाइन फ़ॉर्मैटिंग आती है; स्क्रिप्ट, स्टाइल, फ़ॉर्म कंट्रोल और छिपे हुए एलिमेंट बाहर रहते हैं। फ़्रैगमेंट और पूरा पेज, दोनों चलते हैं (पेज की बॉडी दिखती है)।
  • document: ऐप द्वारा बनाए या रखे गए ब्लॉक: TSX में सादे ऑब्जेक्ट, Rust में nsh_base::text_view::Document (दस्तावेज़ मॉडल)।
use nsh_base::text_view;
use nsh_base::text_view::{Block, Document, Inline};
use nsh_core::prelude::*;

fn shipping() -> impl IntoElement {
    text_view().html(r#"<h2>Shipping</h2><p>Orders ship in <b>2 days</b>. <a href="/faq">FAQ</a></p>"#)
}

fn total() -> impl IntoElement {
    text_view().document(Document::new(vec![
        Block::Heading { level: 2, content: vec![Inline::text("Total")], anchor: None },
        Block::paragraph(vec![Inline::Strong { content: vec![Inline::text("$42.00")] }]),
    ]))
}

TextView अपनी सामग्री के साथ बढ़ता है। लंबे दस्तावेज़ों के लिए इसे Scroll में रखें (या List की किसी पंक्ति में)।

#भागों की स्टाइलिंग

class और style रूट को स्टाइल करते हैं, जो ब्लॉक का एक कॉलम है; वहाँ सेट की गई टेक्स्ट स्टाइल (रंग, आकार, फ़ॉन्ट) हर भाग तक पहुँचती हैं। रूट भी एक भाग है, root: styles.root टेक्स्ट स्टाइल को उनके टाइप के साथ लेता है (style व्यू की तरह टाइप किया गया है), और classes.root क्लासेज़। classes भाग के अनुसार Tailwind क्लासेज़ लेता है और styles भाग के अनुसार इनलाइन स्टाइल। किसी भाग की क्लासेज़ उसकी अपनी डिफ़ॉल्ट स्टाइल के बाद आती हैं, इसलिए वही लागू होती हैं; उसके styles क्लासेज़ पर भारी पड़ते हैं। Rust में भाग टाइप्ड स्टाइल लेते हैं: TextViewParts हर TextViewPart को अपनी स्टाइल निखारने वाला एक फ़ंक्शन देता है, उस व्यू के नीचे के हर TextView के लिए जो उसे उपलब्ध कराता है (टाइप्ड पार्ट स्टाइल), और रूट स्टाइल मेथड लेता है। डिफ़ॉल्ट में लाइन की ऊँचाई पॉइंट में तय है: जो क्लास किसी भाग का आकार बदलती है, वह उसकी लाइन की ऊँचाई भी तय करती है (Tailwind की text-* क्लासेज़ ऐसा करती हैं)।

use nsh_base::{TextViewPart, TextViewParts, text_view, theme};
use nsh_core::prelude::*;

fn reply(markdown: Signal<SharedString>) -> impl IntoElement {
    provide_context(
        TextViewParts::new()
            .part(TextViewPart::Heading, |s| s.text_color(rgb(0x0a0a0a)))
            .part(TextViewPart::Heading2, |s| s.text_xl().mt_2())
            .part(TextViewPart::Blockquote, |s| {
                s.border_l(px(2.0)).border_color(rgb(0xd4d4d4)).pl_3().text_color(rgb(0x525252))
            })
            .part(TextViewPart::CodeBlock, |s| s.rounded_lg().bg(rgb(0xf5f5f5)))
            .part(TextViewPart::CodeBlockHeader, |s| s.px_3().pt_2())
            .part(TextViewPart::CodeBlockCode, |s| s.p_3().text_size(px(13.0)))
            .part(TextViewPart::Table, |s| s.rounded_lg().border_1().border_color(rgb(0xe5e5e5)))
            .part(TextViewPart::TableHead, |s| s.bg(rgb(0xfafafa)))
            .part(TextViewPart::TableCell, |s| {
                s.px_3().py_2().border_t_1().border_color(rgb(0xe5e5e5))
            })
            .part(TextViewPart::TaskMarkerChecked, |s| s.text_color(rgb(0x16a34a)))
            .part(TextViewPart::Link, |s| s.text_color(theme().primary)),
    );
    text_view()
        .markdown(move || markdown.get())
        .gap_3()
        .text_size(px(15.0))
        .line_height(px(24.0))
        .text_color(rgb(0x262626))
        .font_family("Inter")
}

कुछ नोड कई भाग होते हैं, जो क्रम से लागू होते हैं: लेवल 2 की हेडिंग heading और फिर heading2 है; अलर्ट blockquote, alert, फिर उसका प्रकार (alertNote, …) है; टास्क मार्कर listMarker, taskMarker है, और पूरा होने पर taskMarkerChecked; हेडर पंक्ति tableRow फिर tableHead है, और उसकी सेल tableCell फिर tableHeaderCell।

भागRustयह क्या है
rootRootस्वयं टेक्स्ट व्यू, उसके ब्लॉक का एक कॉलम; यहाँ की टेक्स्ट स्टाइल हर भाग तक पहुँचती हैं।
paragraphParagraphहर पैराग्राफ़।
headingHeadingहर हेडिंग, उसके स्तर के भाग से पहले।
heading1 … heading6Heading1 … Heading6उस स्तर की हेडिंग (27, 20, 17, फिर 15pt, सेमीबोल्ड)।
blockquoteBlockquoteकोटेशन और अलर्ट: उनके ब्लॉक का एक कॉलम, किनारे पर एक रेखा के साथ।
alertAlertहर अलर्ट (> [!NOTE]), blockquote के बाद: एक रंगीन बॉक्स, जिसमें उसका आइकन और उसके ब्लॉक की एक पंक्ति होती है।
alertNote, alertTip, alertImportant, alertWarning, alertCautionAlertNote, AlertTip, AlertImportant, AlertWarning, AlertCautionउस प्रकार के अलर्ट।
alertIconAlertIconअलर्ट का आइकन: अलर्ट के रंग में एक ग्लिफ़ वाला रिंग।
listListहर सूची: उसके आइटम का एक कॉलम।
orderedList, unorderedListOrderedList, UnorderedListक्रमांकित और बुलेट वाली सूचियाँ, list के बाद।
listItemListItemहर आइटम: उसके मार्कर और उसके ब्लॉक की एक पंक्ति।
listMarkerListMarkerआइटम का मार्कर (गहराई के अनुसार •, ◦, ▪, या 1.), आइटम से पहले के इंडेंट में दाईं ओर संरेखित।
taskMarkerTaskMarkerटास्क आइटम का मार्कर: व्यू के रूप में बना एक बॉक्स, पूरा होने पर ✓ से भरा हुआ; listMarker के बाद।
taskMarkerCheckedTaskMarkerCheckedपूरे हो चुके टास्क आइटम का मार्कर, taskMarker के बाद।
listItemContentListItemContentआइटम के ब्लॉक का कॉलम।
codeBlockCodeBlockकोड ब्लॉक: कोड का एक बॉर्डर वाला, गोल बॉक्स, जिसके ऊपर हेडर होता है।
codeBlockHeaderCodeBlockHeaderकोड ब्लॉक की क्रियाओं (codeBlockActions) की पंक्ति, जो उसके ऊपरी दाएँ कोने पर तैरती है।
codeBlockScrollCodeBlockScrollकोड ब्लॉक के कोड के चारों ओर का क्षैतिज स्क्रॉल व्यू।
codeBlockCodeCodeBlockCodeकोड ब्लॉक का कोड (मोनोस्पेस, जैसा लिखा गया हो)।
codeCodeटेक्स्ट में कोड (मोनोस्पेस, गोल बैकग्राउंड पर)।
keyboardKeyboardकीज़ (<kbd>, कोड की तरह)।
strongStrongस्ट्रॉन्ग टेक्स्ट (सेमीबोल्ड)।
emphasisEmphasisइमफ़ेसाइज़ किया हुआ टेक्स्ट (इटैलिक)।
strikethroughStrikethroughस्ट्राइक-थ्रू टेक्स्ट।
underlineUnderlineरेखांकित टेक्स्ट (<u>, <ins>)।
highlightHighlightहाइलाइट किया हुआ टेक्स्ट (<mark>, गोल पीले बैकग्राउंड पर)।
superscript, subscriptSuperscript, Subscriptसुपरस्क्रिप्ट और सबस्क्रिप्ट (छोटे)।
linkLinkलिंक (थीम के primary रंग में, रेखांकित)।
imageImageइमेज।
mathMathटेक्स्ट में फ़ॉर्मूले, जो renderMath न होने पर टाइपसेट होते हैं।
mathBlockMathBlockफ़ॉर्मूला ब्लॉक, जो renderMath न होने पर टाइपसेट और केंद्रित होते हैं।
tableTableटेबल: renderTableActions की सामग्री, फिर सेल का स्क्रॉल व्यू।
tableScrollTableScrollटेबल की सेल का क्षैतिज स्क्रॉल व्यू: जगह होने पर कॉलम अपनी सामग्री के अनुसार फैलते हैं, व्यू संकरा होने पर एक न्यूनतम सीमा तक रैप होते हैं, और उसके बाद टेबल स्क्रॉल होती है।
tableRowTableRowटेबल की हर पंक्ति: उसकी सेल के पीछे एक व्यू, बैकग्राउंड और बॉर्डर के लिए (टेक्स्ट स्टाइल tableCell पर जाती हैं)।
tableHeadTableHeadहेडर पंक्ति, tableRow के बाद।
tableCellTableCellहर सेल।
tableHeaderCellTableHeaderCellहेडर पंक्ति की सेल (छोटी, धीमे रंग की), tableCell के बाद।
ruleRuleथीमैटिक ब्रेक: ऊपरी बॉर्डर के रूप में बनी एक रेखा।
detailsDetailsडिस्क्लोज़र (<details>)।
summarySummaryडिस्क्लोज़र का सारांश; प्रेस करने पर वह खुलता और बंद होता है।
summaryMarkerSummaryMarkerसारांश का मार्कर (▸, ▾)।
footnotesFootnotesब्लॉक के बाद के फ़ुटनोट: एक कॉलम।
footnoteFootnoteएक फ़ुटनोट: उसके नंबर और उसके ब्लॉक की एक पंक्ति।
footnoteLabelFootnoteLabelफ़ुटनोट का नंबर।
footnoteReferenceFootnoteReferenceटेक्स्ट में फ़ुटनोट का संदर्भ (उसका नंबर, छोटा)।
customCustomअपनी सामग्री के रूप में दिखाए गए कस्टम नोड (जब कोई रेंडर फ़ंक्शन उन्हें न दिखाए)।

#AI उत्तरों की स्ट्रीमिंग

उत्तर को जैसे-जैसे वह बढ़ता है, options={{ streaming: true }} (Rust में MarkdownOptions { streaming: true, .. }) के साथ पास करें। अभी आ रहे टेक्स्ट के अंत में अक्सर सिंटैक्स खुला रह जाता है (**bold, `code, [a link](htt, a table whose header just arrived); स्ट्रीमिंग उसे वैसा दिखाती है जैसा वह बंद होने पर दिखेगा, इसलिए पाठक को कभी बेमतलब के ** या [ नहीं दिखते। ब्लॉक स्थिति के अनुसार रखे जाते हैं: जो ब्लॉक पूरे हो चुके हैं वे अपने व्यू रखते हैं, और केवल लिखा जा रहा ब्लॉक दोबारा बनाया जाता है, इसलिए लंबे उत्तर को अपडेट करना सस्ता रहता है।

streamingCaret दस्तावेज़ के अंत का अनुसरण करता है: इनलाइन, अंतिम टेक्स्ट के अंत में, जहाँ भी अंतिम ब्लॉक समाप्त हो (पैराग्राफ़, सूची आइटम, कोड ब्लॉक, टेबल की अंतिम सेल), अन्यथा अंतिम ब्लॉक (रेखा, रेंडर फ़ंक्शन की सामग्री) के बाद अपनी अलग पंक्ति में। उत्तर पूरा होने पर इसे हटा दें।

use nsh_base::{MarkdownOptions, text_view};
use nsh_core::prelude::*;

/// A reply the app appends to as chunks arrive, with `done` set at the end.
fn reply(text: Signal<SharedString>, done: Signal<bool>) -> impl IntoElement {
    text_view()
        .markdown(move || text.get())
        .options(move || MarkdownOptions { streaming: !done.get(), ..Default::default() })
        .streaming_caret(when(
            move || !done.get(),
            || view().ml_0p5().h_4().w_2().rounded_sm().bg(rgb(0xa3a3a3)),
        ))
}

कैरेट ऐप का अपना व्यू है, इसलिए वह nsh के एनिमेशन API से ब्लिंक या पल्स कर सकता है, और नए ब्लॉक ऐप के एनिमेशन से फ़ेड-इन हो सकते हैं।

#लिंक

लिंक प्रेस किए जा सकने वाले स्पैन हैं। उन पर प्रेस करने से onLinkPress को { url } के साथ कॉल किया जाता है; क्या खोलना है, यह ऐप तय करता है (अपना कोई पेज, एक WebView, किसी हेडिंग तक स्क्रॉल)। हेडिंग में एंकर होते हैं (GitHub वाले: ## Getting started का एंकर #getting-started है), इसलिए #getting-started का लिंक उसी स्ट्रिंग के रूप में आता है, और फ़ुटनोट का संदर्भ #footnote-<label> के रूप में आता है।

use nsh_base::{TextViewLinkPressEvent, text_view};
use nsh_core::prelude::*;

fn doc(markdown: &'static str) -> impl IntoElement {
    text_view().markdown(markdown).on_link_press(|TextViewLinkPressEvent { url, .. }: &TextViewLinkPressEvent| {
        match url.strip_prefix('#') {
            Some(anchor) => scroll_to_anchor(anchor),
            None => open_web(url),
        }
    })
}

scroll_to_anchor और open_web (TSX में scrollToAnchor और nav) ऐप के अपने फ़ंक्शन के प्रतीक हैं।

links="open" के साथ, किसी दूसरी साइट या ऐप के लिंक (स्कीम वाला URL: https:, mailto:, …) पर प्रेस करने से वह ऐप के बाहर भी खुलता है, openUrl से, ऐप की openUrl क्षमता के तहत, onLinkPress के उसे रिपोर्ट करने के बाद; दस्तावेज़ के अंदर के एंकर (#…) केवल रिपोर्ट किए जाते हैं। डिफ़ॉल्ट, "report", कुछ नहीं खोलता।

use nsh_base::{TextViewLinks, text_view};

text_view().markdown(doc).links(TextViewLinks::Open)

#रेंडर फ़ंक्शन

रेंडर फ़ंक्शन ऐप की अपनी सामग्री को दस्तावेज़ में रखते हैं। हर एक किसी भाग की वैल्यू लेता है और उसकी सामग्री लौटाता है (TSX में JSX, Rust में एक एलिमेंट); जब तक भाग की वैल्यू वही रहती है, भाग उसके लिए रेंडर की गई सामग्री को रखता है, इसलिए स्ट्रीमिंग के दौरान केवल लिखा जा रहा भाग दोबारा रेंडर होता है।

प्रॉपRustवैल्यूक्या बदलता या जोड़ता है
renderCodeBlock.render_code_block(|code| ..)TextViewCodeBlock (CodeBlock)कोड ब्लॉक को बदलता है (आपकी अपनी सिंटैक्स हाइलाइटिंग)।
renderCodeBlockAction.render_code_block_action(|action| ..)TextViewCodeActionऐप की किसी कोड ब्लॉक क्रिया (codeBlockActions) का आइकन; उसके चारों ओर का बटन TextView का है।
renderTableActions.render_table_actions(|table| ..)TextViewTable (Table)टेबल के ऊपर सामग्री जोड़ता है (कॉपी, एक्सपोर्ट)।
renderMath.render_math(|math| ..)TextViewMathफ़ॉर्मूले को बदलता है: { tex, display }, $$…$$ ब्लॉक के लिए display।
renderImage.render_image(|image| ..)TextViewImage (Image)इमेज ({ src, alt, title }) को बदलता है: आपकी अपनी लोडिंग, साइज़िंग, प्रेस पर व्यूअर।
renderCustom.render_custom(|node| ..)TextViewCustomNode (CustomNode)कस्टम नोड को उसके name से दिखाता है (Markdown एक्सटेंशन)।

कोड ब्लॉक के ऊपरी दाएँ कोने पर क्रियाएँ होती हैं, सभी एक जैसी बनी हुई: घोस्ट बटन, जिनका बैकग्राउंड होवर या प्रेस के दौरान दिखता है, और जो डेस्कटॉप पर तब दिखते हैं जब पॉइंटर ब्लॉक के ऊपर हो, और टच स्क्रीन पर हमेशा। copy बिल्ट-इन है: वह कोड कॉपी करता है और कुछ पल के लिए चेक दिखाता है। बाकी क्रियाएँ ऐप की हैं: renderCodeBlockAction उनका आइकन बनाता है और onCodeBlockAction उनके प्रेस रिपोर्ट करता है (बिल्ट-इन copy भी, कॉपी करने के बाद)। copy क्लिपबोर्ड को वैसे ही लिखता है जैसे Clipboard, इसलिए उसे ऐप की clipboard क्षमता (nsh.json) चाहिए।

use nsh_base::{TextViewCodeAction, TextViewCodeActionEvent, text_view};
use nsh_core::prelude::*;

fn reply(markdown: Signal<SharedString>) -> impl IntoElement {
    text_view()
        .markdown(move || markdown.get())
        .code_block_actions(vec![
            TextViewCodeAction::new("copy").with_label("Copy code"),
            TextViewCodeAction::new("run").with_label("Run"),
        ])
        .render_code_block_action(|_action: &TextViewCodeAction| text("▶").text_xs())
        .on_code_block_action(|event: &TextViewCodeActionEvent| {
            if event.id == "run" {
                run(&event.code.code);
            }
        })
}

कोड ब्लॉक पहले से हाइलाइट होते हैं: फ़ेंस पर बताई गई भाषा (ts, tsx, rust, python, bash, json, toml, yaml, diff और लगभग दो सौ और) के लिए कोड Allsum के कोड पैलेट में दिखता है, अपीयरेंस के अनुसार लाइट या डार्क; बिना भाषा वाला ब्लॉक सादा दिखता है। जिस ऐप की अपनी हाइलाइटिंग है, वह अपना कोड renderCodeBlock से दिखाता है, अपने कॉम्पोनेंट से या अपने Rust क्रेट के किसी कॉम्पोनेंट से; Rust व्यू TextView को उसका हाइलाइटर CodeHighlighter से देता है। यहाँ ऐप की अपनी हाइलाइटिंग वाला कोड है, और एक इमेज जो पूरी चौड़ाई भरती है:

use nsh_base::text_view;
use nsh_base::text_view::{CodeBlock, Image};
use nsh_base::TextViewMath;
use nsh_core::prelude::*;

fn reply(markdown: Signal<SharedString>) -> impl IntoElement {
    text_view()
        .markdown(move || markdown.get())
        .render_code_block(|code: &CodeBlock| highlighted(code.language.clone(), code.code.clone()))
        .render_image(|img: &Image| {
            image(img.src.clone())
                .label(img.alt.clone())
                .aspect_ratio(16.0 / 9.0)
                .w_full()
                .rounded_lg()
        })
        .render_math(|math: &TextViewMath| math_view(math.tex.to_string(), math.display))
}

Highlighted और MathView (Rust में highlighted और math_view) ऐप के अपने कॉम्पोनेंट के प्रतीक हैं (हाइलाइटर, फ़ॉर्मूला रेंडरर, या उसके Rust क्रेट का कोई कॉम्पोनेंट)। renderMath के बिना TextView फ़ॉर्मूले को खुद टाइपसेट करता है (math और mathBlock भाग), KaTeX के TeX सपोर्ट और फ़ॉन्ट के साथ, टेक्स्ट के रंग में, हर प्लेटफ़ॉर्म पर एक जैसा; जो TeX वह पढ़ नहीं पाता, वह मोनोस्पेस में अपने सोर्स के रूप में दिखता है।

#Markdown एक्सटेंशन

extensions ऐप की अपनी सिंटैक्स को कस्टम नोड में बदलता है, जिन्हें renderCustom दिखाता है। हर एक्सटेंशन में एक name (उसके बनाए नोड का name) और चार प्रकार की सिंटैक्स में से एक होती है:

प्रकारRustमिलाननोड
patternMarkdownExtension::pattern(name, regex)टेक्स्ट में एक रेगुलर एक्सप्रेशन (Rust की regex सिंटैक्स), कोड और लिंक के बाहरइनलाइन; text मिलान है, attributes उसके समूह: "1", "2", … और नामित समूह अपने नाम से
languageMarkdownExtension::language(name, language)उस भाषा के fenced कोड ब्लॉक (```chart)A block; text is the code, attributes.meta info string का शेष भाग
tagMarkdownExtension::tag(name, tag)उस टैग के HTML या MDX एलिमेंट, किसी भी केस में (<Callout kind="tip">)उसके एट्रिब्यूट, उसकी पार्स की गई सामग्री: children (ब्लॉक) अपनी अलग पंक्तियों वाले एलिमेंट के लिए, content (इनलाइन) टेक्स्ट के भीतर
containerMarkdownExtension::container(name, container)कंटेनर :::name … :::भीतर के Markdown का एक ब्लॉक (children); :::note[Heads up] से attributes.label मिलता है, {key=value} बाकी एट्रिब्यूट देता है
use nsh_base::text_view::{CustomNode, Document};
use nsh_base::{MarkdownExtension, text_view};
use nsh_core::prelude::*;

const MESSAGE: &str = r#"Thanks @ada! Here are the numbers:

```chart
12, 19, 7, 25
```

<Callout kind="tip">Charts update live.</Callout>

:::note[Heads up]
Numbers are in thousands.
:::"#;

fn custom(node: &CustomNode) -> AnyElement {
    match node.name.as_str() {
        "mention" => text(node.text.clone())
            .font_weight(FontWeight::SEMIBOLD)
            .text_color(rgb(0x2563eb))
            .into_element(),
        "chart" => bar_chart(node.text.split(',').filter_map(|v| v.trim().parse().ok()).collect())
            .into_element(),
        "callout" => view()
            .rounded_lg()
            .bg(rgb(0xfffbeb))
            .p_3()
            .child(text_view().document(Document::new(node.children.clone())))
            .into_element(),
        _ => view()
            .gap_1()
            .border_l(px(4.0))
            .border_color(rgb(0x3b82f6))
            .pl_3()
            .child(
                text(node.attributes.get("label").cloned().unwrap_or_else(|| "Note".into()))
                    .font_weight(FontWeight::SEMIBOLD),
            )
            .child(text_view().document(Document::new(node.children.clone())))
            .into_element(),
    }
}

fn message() -> impl IntoElement {
    text_view()
        .markdown(MESSAGE)
        .extensions(vec![
            MarkdownExtension::pattern("mention", r"@(\w+)"),
            MarkdownExtension::language("chart", "chart"),
            MarkdownExtension::tag("callout", "callout"),
            MarkdownExtension::container("note", "note"),
        ])
        .render_custom(custom)
}

BarChart (Rust में bar_chart) ऐप के अपने चार्ट को दर्शाता है। मेंशन पैटर्न का समूह node.attributes?.["1"] में है (Rust में node.attributes["1"]: ada) और पूरा मिलान node.text (@ada) में है। जिस कस्टम नोड का कोई रेंडर फ़ंक्शन नहीं होता, वह अपनी सामग्री दिखाता है। इनलाइन नोड (चिप) के लिए लौटाया गया व्यू अपनी verticalAlign के अनुसार अपनी पंक्ति में बैठता है: align-middle (verticalAlign: "middle") उसे टेक्स्ट के केंद्र में रखता है, और उसकी पंक्ति पैराग्राफ़ की lineHeight बनाए रखती है, जब तक कि व्यू उससे ऊँचा न हो।

#विकल्प

options चुनता है कि CommonMark और GitHub की सिंटैक्स के अलावा कौन-सी Markdown सिंटैक्स पढ़ी जाए; Rust में यह उन्हीं फ़ील्ड वाला एक MarkdownOptions है (MarkdownOptions { streaming: true, ..Default::default() }):

कुंजीप्रकारडिफ़ॉल्टविवरण
mathbooleantrueसूत्र: टेक्स्ट में $…$, ब्लॉक में $$…$$।
htmlbooleantrueMarkdown के भीतर का HTML वैसे ही पढ़ा जाता है जैसे html प्रॉप उसे पढ़ता है; false उसे टेक्स्ट के रूप में दिखाता है।
alertsbooleantrueGitHub के अलर्ट: > [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION]।
frontmatterbooleanfalseYAML front matter (सबसे पहले --- … ---) एक कस्टम नोड frontmatter बन जाता है, जिसका text वह YAML है।
mdxbooleanfalseMDX: JSX एलिमेंट अपने टैग के नाम वाले कस्टम नोड बनते हैं, {expressions} कस्टम नोड expression; इसके बाद raw HTML टेक्स्ट के रूप में पढ़ा जाता है।
streamingbooleanfalseटेक्स्ट अभी आ रहा है: जो सिंटैक्स उसके अंत में खुली रह जाती है, वह ऐसे दिखती है जैसे बंद हो।

#दस्तावेज़ मॉडल

document प्रॉप और रेंडर फ़ंक्शनों के मान सामान्य ऑब्जेक्ट हैं, जिनमें हर नोड अपने type से टैग किया गया है। टाइप nsh/base से आते हैं: TextViewDocument, TextViewBlock, TextViewInline, TextViewCodeBlock, TextViewTable, TextViewListItem, TextViewImage, TextViewFootnote, TextViewAlert और TextViewCustomNode। Rust में ये nsh_base::text_view के टाइप हैं (Document, Block, Inline, CodeBlock, Table, ListItem, Image, Footnote, Alert, CustomNode): Block और Inline ऐसे enum हैं जिनके variant नीचे दिए गए टाइप हैं, PascalCase में (Block::Heading { level, content, anchor }, Inline::Strong { content }), और उनके फ़ील्ड वही हैं, snake case में।

ब्लॉक typeफ़ील्ड
paragraphcontent
headinglevel (1–6), content, anchor?
blockquotechildren, alert? ("note", "tip", "important", "warning", "caution")
listordered?, start?, loose?, items ({ checked?, children })
codeBlocklanguage?, meta?, code
mathtex
tablealign? (हर कॉलम के लिए "none", "left", "center", "right"), head (सेल), rows? (सेल की पंक्तियाँ); एक सेल { content } है
rule
detailsopen?, summary (इनलाइन), children
customname, attributes?, text?, data?, children?, content?
इनलाइन typeफ़ील्ड
texttext
strong, emphasis, strikethrough, underline, highlight, superscript, subscriptcontent
codecode
keyboardtext
linkurl, title?, content
imagesrc, alt?, title?
mathtex
break
footnoteReferencelabel, index
customब्लॉक के समान

एक दस्तावेज़ में footnotes? भी होता है: { label, index, children }, जो ब्लॉक के बाद उनके पहले संदर्भ के क्रम में दिखाया जाता है। एक ऐप उत्तरों को दस्तावेज़ के रूप में संग्रहीत कर सकता है, उन्हें अपने डेटा से बना सकता है, या ऐसे कस्टम नोड मिला सकता है जिन्हें renderCustom दिखाता है।

#पार्सिंग और सादा टेक्स्ट

TextView दस्तावेज़ों को बिना दिखाए, सिंक्रोनस रूप से भी पढ़ता है:

फ़ंक्शनRustलौटाता है
TextView.parse(markdown, options?, extensions?)TextView::parse(markdown, options, extensions)वह TextViewDocument जो markdown प्रॉप दिखाता।
TextView.parseHtml(html, extensions?)TextView::parse_html(html, extensions)वह TextViewDocument जो html प्रॉप दिखाता।
TextView.plainText(document)TextView::plain_text(document), Document::plain_text()मार्कअप के बिना दस्तावेज़ का टेक्स्ट: ब्लॉक खाली पंक्तियों से अलग, सूची आइटम और टेबल की पंक्तियाँ लाइन ब्रेक से अलग, सेल टैब से अलग।

पूरे उत्तर के लिए एक कॉपी बटन:

use nsh_base::{TextView, button};
use nsh_core::prelude::*;

fn copy_reply(reply: Signal<SharedString>) -> impl IntoElement {
    button()
        .on_press(move || {
            let document = TextView::parse(reply.get().to_string(), None, None);
            set_clipboard(TextView::plain_text(document));
        })
        .child(text("Copy"))
}

एक ऐप एक बार पार्स करके दस्तावेज़ को रख भी सकता है (document={doc}), या उसे दिखाने से पहले बदल सकता है।

#Rust में

Rust व्यू nsh-base क्रेट के उसी कंपोनेंट का उपयोग करते हैं, ऊपर के उदाहरण उनके Rust टैब में हैं। तीन चीज़ें Rust की अपनी हैं: ऐप का हाइलाइटर, भाग के अनुसार टाइप्ड स्टाइल, और Markdown प्लगइन; इनमें से हर एक को व्यू के नीचे के हर TextView को provide_context से दिया जाता है। दस्तावेज़ के टाइप nsh_base::text_view के अंतर्गत हैं, जहाँ Parser प्लगइन के साथ दस्तावेज़ भी पढ़ता है (Parser::new().plugin(..).markdown(source))।

#सिंटैक्स हाइलाइटिंग

TextView कोड को syntect से Allsum के कोड पैलेट में हाइलाइट करता है (nsh-base का फ़ीचर highlight, डिफ़ॉल्ट रूप से चालू); बड़ा ब्लॉक पहले सादा दिखता है और बैकग्राउंड थ्रेड द्वारा हाइलाइट किए जाने पर अपने रंग लेता है। CodeHighlighter उसकी जगह ऐप का अपना हाइलाइटर है, जो व्यू के नीचे के हर TextView को provide_context से दिया जाता है। उसे हर कोड ब्लॉक मिलता है और वह अपने कोड की बाइट रेंज के स्टाइल लौटाता है (क्रम में, ओवरलैप किए बिना; जो वह छोड़ देता है वह सादा रहता है); TextView कोड को उन स्टाइल वाले स्पैन के रूप में दिखाता है, अपने सामान्य कोड ब्लॉक (हेडर, एक्शन, स्क्रॉल व्यू, भाग) के भीतर। हाइलाइटर कुछ भी हो सकता है जो कोड को स्टाइल की गई रेंज में बदले: tree-sitter, ऐप की अपनी थीम वाला syntect, …

use std::ops::Range;

use nsh_base::{CodeHighlighter, text_view::CodeBlock};
use nsh_core::{element::StyleRefinement, prelude::*};

/// The app's highlighter (here with syntect, set up once by the app).
fn highlight(code: &CodeBlock) -> Vec<(Range<usize>, StyleRefinement)> {
    my_syntect::ranges(code.language.as_deref(), &code.code)
        .map(|(range, color, bold)| {
            let style = StyleRefinement::default().text_color(color);
            (range, if bold { style.font_weight(FontWeight::BOLD) } else { style })
        })
        .collect()
}

fn chat() -> impl IntoElement {
    provide_context(CodeHighlighter::new(highlight));
    view().child(messages())
}

my_syntect::ranges ऐप के हाइलाइटर से जोड़ने वाले कोड को दर्शाता है और messages चैट के ऐप के व्यू को। renderCodeBlock, यदि दिया गया हो, तो इस सबकी जगह कोड ब्लॉक को दिखाता है।

#टाइप्ड पार्ट स्टाइल

TextViewParts Rust व्यू के लिए भागों को क्लास स्ट्रिंग की जगह टाइप्ड स्टाइल से स्टाइल करता है: हर TextViewPart (ऊपर की तालिका के हर भाग के लिए एक) को एक फ़ंक्शन मिलता है जो उसकी StyleRefinement को परिष्कृत करता है। provide_context से दिए जाने पर, वे नीचे के हर TextView पर लागू होते हैं, उसके classes और styles से पहले; उनमें पढ़े गए थीम रंग लाइट और डार्क अपीयरेंस के अनुसार बदलते हैं।

use nsh_base::{TextViewPart, TextViewParts, theme};
use nsh_core::prelude::*;

fn chat_parts() {
    provide_context(
        TextViewParts::new()
            .part(TextViewPart::Heading1, |s| s.text_2xl().font_weight(FontWeight::BOLD))
            .part(TextViewPart::Code, |s| s.bg(theme().muted).rounded(px(4.0)))
            .part(TextViewPart::Link, |s| s.text_color(theme().primary)),
    );
}

#Markdown प्लगइन

एक MarkdownPlugin Rust में Markdown सिंटैक्स पढ़ता है: पार्सर के पढ़ने से पहले उसे सिंटैक्स ट्री का हर नोड मिलता है (markdown-rs का mdast), और वह अपनी ब्लॉक या इनलाइन सामग्री लौटाता है, आमतौर पर एक CustomNode; वह अपने बनाए कस्टम नोड भी दिखा सकता है। एक व्यू अपने नीचे के हर TextView को प्लगइन देता है:

use nsh_base::text_view::{mdast, CustomNode, Inline, MarkdownPlugin, MarkdownPlugins, ParseContext};
use nsh_core::prelude::*;

struct Tickers;

impl MarkdownPlugin for Tickers {
    fn name(&self) -> &str {
        "tickers"
    }

    /// `$AAPL` in inline code becomes a ticker node.
    fn inline(&self, node: &mdast::Node, _cx: &mut ParseContext<'_>) -> Option<Inline> {
        let mdast::Node::InlineCode(code) = node else { return None };
        let symbol = code.value.strip_prefix('$')?;
        Some(Inline::Custom(CustomNode::new("ticker").with_text(symbol)))
    }

    fn render(&self, node: &CustomNode) -> Option<AnyElement> {
        (node.name == "ticker").then(|| text(node.text.clone()).font_weight(FontWeight::BOLD).into_element())
    }
}

fn chat() -> impl IntoElement {
    provide_context(MarkdownPlugins::new().with(Tickers));
    view().child(messages())
}

प्लगइन का render पहले आता है, फिर renderCustom, फिर नोड की सामग्री। configure markdown-rs की और सिंटैक्स चालू करता है, और ParseContext किसी नोड के चिल्ड्रन को वैसे पढ़ता है जैसे पार्सर पढ़ता है।

#प्रॉप

प्रॉपRustप्रकारडिफ़ॉल्टविवरण
markdown.markdown(..)stringMarkdown के रूप में दस्तावेज़।
html.html(..)stringHTML के रूप में दस्तावेज़, जब markdown न दिया गया हो।
document.document(..)TextViewDocumentब्लॉक के रूप में दस्तावेज़, जब न markdown दिया गया हो न html।
options.options(..)MarkdownOptionsसभी डिफ़ॉल्टपढ़ी जाने वाली Markdown सिंटैक्स (विकल्प)।
extensions.extensions(..)MarkdownExtension[][]ऐप की अपनी सिंटैक्स, कस्टम नोड के रूप में (Markdown एक्सटेंशन)।
classes.classes(..)TextViewClasses{}भाग के अनुसार Tailwind क्लास।
styles.styles(..)TextViewStyles{}भाग के अनुसार इनलाइन स्टाइल (हर एक StyleProp<TextStyle>), क्लास के ऊपर।
selectable.selectable(..)booleanfalseदस्तावेज़ एक इकाई के रूप में चुना जा सकता है: चयन उसके पैराग्राफ़, हेडिंग, सेल और कोड ब्लॉक के आर-पार चलता है (टेक्स्ट चयन)।
links.links(..)"report" | "open""report"लिंक पर प्रेस करने से क्या होता है: वह उसकी रिपोर्ट करता है (onLinkPress), और "open" के साथ किसी दूसरी साइट या ऐप के लिंक को ऐप के बाहर खोलता भी है (लिंक)।
codeBlockScrollbar.code_block_scrollbar(..)booleantrueकोड ब्लॉक अपना क्षैतिज स्क्रॉल संकेतक दिखाते हैं।
tableScrollbar.table_scrollbar(..)booleantrueटेबल अपना क्षैतिज स्क्रॉल संकेतक दिखाते हैं।
streamingCaret.streaming_caret(..)JSX.Elementदस्तावेज़ के अंत में दिखाया जाता है: अंतिम टेक्स्ट के बाद इनलाइन, अन्यथा अपनी अलग पंक्ति में।
renderCustom.render_custom(|node| ..)(node: TextViewCustomNode) => JSX.Elementकस्टम नोड दिखाता है।
renderCodeBlock.render_code_block(|code| ..)(code: TextViewCodeBlock) => JSX.Elementकोड ब्लॉक दिखाता है।
codeBlockActions.code_block_actions(..)TextViewCodeAction[][]हर कोड ब्लॉक के ऊपरी दाएँ कोने पर एक्शन: { id: "copy" } बिल्ट-इन है, बाकी ऐप के अपने।
renderCodeBlockAction.render_code_block_action(|action| ..)(action: TextViewCodeAction) => JSX.Elementऐप के अपने एक्शन का आइकन।
renderTableActions.render_table_actions(|table| ..)(table: TextViewTable) => JSX.Elementहर टेबल के ऊपर की सामग्री।
renderMath.render_math(|math| ..)(math: TextViewMath) => JSX.Elementसूत्र दिखाता है।
renderImage.render_image(|image| ..)(image: TextViewImage) => JSX.Elementइमेज दिखाता है।
class, styleस्टाइल मेथडstring, StyleProp<ViewStyle>रूट, ब्लॉक का एक कॉलम; उसके टेक्स्ट स्टाइल हर भाग को पास होते हैं (टाइप्ड टेक्स्ट स्टाइल: styles.root)।

Rust में भाग के अनुसार क्लास और स्टाइल TextViewClasses और TextViewStyles हैं; Rust व्यू आमतौर पर भागों को TextViewParts से स्टाइल करता है।

#इवेंट

इवेंटRustविवरण
onLinkPress.on_link_press(|event: &TextViewLinkPressEvent| ..)किसी लिंक पर प्रेस किया गया: { url }: उसका URL, हेडिंग के लिए #anchor, फ़ुटनोट के लिए #footnote-<label>।
onCodeBlockAction.on_code_block_action(|event: &TextViewCodeActionEvent| ..)किसी कोड ब्लॉक के एक्शन पर प्रेस किया गया (copy के कॉपी करने के बाद)।
onImagePress.on_image_press(|image: &Image| ..)लिंक में न होने वाली किसी इमेज पर प्रेस किया गया: { image }, ताकि ऐप उसे बड़ा दिखा सके (अपना ज़ूम करने योग्य व्यूअर)।

#एक्सेसिबिलिटी

स्क्रीन रीडर दस्तावेज़ को क्रम से, टेक्स्ट दर टेक्स्ट पढ़ते हैं। TextView संरचना को उसकी भूमिकाएँ देता है: हेडिंग हेडर हैं, लिंक लिंक हैं (उनका शीर्षक संकेत है), टास्क मार्कर अपनी चेक्ड स्थिति के साथ चेकबॉक्स हैं, डिस्क्लोज़र का सारांश एक बटन है जो बताता है कि वह विस्तारित है या नहीं, और इमेज अपने alt टेक्स्ट से पढ़ी जाती हैं।

#संबंधित

  • Text: वह एलिमेंट जो TextView का हर टेक्स्ट है, अपने स्पैन, लिंक, इनलाइन अटैचमेंट और चयन के साथ।
  • टेक्स्ट चयन: कई TextView और टेक्स्ट के आर-पार एक चयन।
  • nsh/base: अन्य बेस कंपोनेंट।
  • Styling और Tailwind।