Bagaimana cara menguraikan subset kecil Markdown ke dalam komponen Bereaksi?


9

Saya memiliki subset Markdown yang sangat kecil bersama dengan beberapa html kustom yang ingin saya parse menjadi komponen Bereaksi. Misalnya, saya ingin mengubah string berikut ini:

hello *asdf* *how* _are_ you !doing! today

Ke dalam array berikut:

[ "hello ", <strong>asdf</strong>, " ", <strong>how</strong>, " ", <em>are</em>, " you ", <MyComponent onClick={this.action}>doing</MyComponent>, " today" ]

dan kemudian mengembalikannya dari fungsi React render (React akan merender array dengan benar sebagai HTML yang diformat)

Pada dasarnya, saya ingin memberi pengguna pilihan untuk menggunakan set Markdown yang sangat terbatas untuk mengubah teks mereka menjadi komponen gaya (dan dalam beberapa kasus komponen saya sendiri!)

Tidak bijaksana untuk berbahaya SetInnerHTML, dan saya tidak ingin membawa ketergantungan eksternal, karena mereka semua sangat berat, dan saya hanya perlu fungsionalitas yang sangat dasar.

Saat ini saya melakukan sesuatu seperti ini, tetapi sangat rapuh, dan tidak berfungsi untuk semua kasus. Saya bertanya-tanya apakah ada cara yang lebih baik:

function matchStrong(result, i) {
  let match = result[i].match(/(^|[^\\])\*(.*)\*/);
  if (match) { result[i] = <strong key={"ms" + i}>{match[2]}</strong>; }
  return match;
}

function matchItalics(result, i) {
  let match = result[i].match(/(^|[^\\])_(.*)_/); // Ignores \_asdf_ but not _asdf_
  if (match) { result[i] = <em key={"mi" + i}>{match[2]}</em>; }
  return match;
}

function matchCode(result, i) {
  let match = result[i].match(/(^|[^\\])```\n?([\s\S]+)\n?```/);
  if (match) { result[i] = <code key={"mc" + i}>{match[2]}</code>; }
  return match;
}

// Very brittle and inefficient
export function convertMarkdownToComponents(message) {
  let result = message.match(/(\\?([!*_`+-]{1,3})([\s\S]+?)\2)|\s|([^\\!*_`+-]+)/g);

  if (result == null) { return message; }

  for (let i = 0; i < result.length; i++) {
    if (matchCode(result, i)) { continue; }
    if (matchStrong(result, i)) { continue; }
    if (matchItalics(result, i)) { continue; }
  }

  return result;
}

Ini pertanyaan saya sebelumnya yang mengarah ke pertanyaan ini.


1
Bagaimana jika input memiliki item bersarang, seperti font _italic *and bold* then only italic_ and normal? Apa yang akan menjadi hasil yang diharapkan? Atau akankah itu tidak pernah disarangkan?
trincot

1
Tidak perlu khawatir tentang bersarang. Ini hanya penurunan harga yang sangat mendasar bagi pengguna untuk digunakan. Apa pun yang paling mudah diterapkan adalah baik-baik saja bagi saya. Dalam contoh Anda, itu akan baik-baik saja jika pencetakan bagian dalam tidak berfungsi. Tetapi jika lebih mudah untuk mengimplementasikan sarang daripada tidak memilikinya maka tidak apa-apa juga.
Ryan Peschel

1
Mungkin paling mudah untuk menggunakan solusi di luar rak seperti npmjs.com/package/react-markdown-it
mb21

1
Saya tidak menggunakan penurunan harga. Itu hanya subset yang sangat mirip / kecil (yang mendukung beberapa komponen khusus, bersama dengan huruf tebal, miring, kode, garis bawah). Cuplikan yang saya poskan agak bekerja, tetapi tampaknya tidak terlalu ideal, dan gagal dalam beberapa kasus sepele, (seperti Anda tidak dapat mengetikkan asterik tunggal seperti ini: asdf*tanpa menghilang)
Ryan Peschel

1
well ... parsing markdown atau sesuatu seperti markdown bukanlah tugas yang mudah ... regex tidak memotongnya ... untuk pertanyaan serupa tentang html, lihat stackoverflow.com/questions/1732348/…
mb21

Jawaban:


1

Bagaimana itu bekerja?

Ini bekerja dengan membaca sepotong string demi sepotong, yang mungkin bukan solusi terbaik untuk string yang sangat panjang.

Setiap kali parser mendeteksi bongkahan kritis sedang dibaca, yaitu '*'atau tag penurunan harga lainnya, ia mulai menguraikan potongan elemen ini hingga parser menemukan tag penutupnya.

Ini berfungsi pada string multi-line, lihat kode misalnya.

Peringatan

Anda belum menentukan, atau saya bisa salah memahami kebutuhan Anda, jika ada kebutuhan untuk mengurai tag yang tebal dan miring , solusi saya saat ini mungkin tidak berfungsi dalam kasus ini.

Jika Anda perlu, untuk bekerja dengan kondisi di atas, cukup komentari di sini dan saya akan mengubah kode.

Pembaruan pertama: tweak bagaimana perlakuan markdown diperlakukan

Tag tidak lagi hardcoded, sebaliknya mereka adalah peta di mana Anda dapat dengan mudah memperluas agar sesuai dengan kebutuhan Anda.

Memperbaiki bug yang Anda sebutkan di komentar, terima kasih telah menunjukkan masalah ini = hal

Pembaruan kedua: tag markdown multi-panjang

Cara termudah untuk mencapai ini: mengganti multi-panjang karakter dengan unicode yang jarang digunakan

Meskipun metode parseMarkdownini belum mendukung tag multi-panjang, kami dapat dengan mudah mengganti tag multi-panjang dengan sederhana string.replace ketika mengirim rawMarkdownprop kami .

Untuk melihat contohnya dalam praktik ini, lihat ReactDOM.render, yang terletak di akhir kode.

Bahkan jika aplikasi Anda mendukung banyak bahasa, ada karakter unicode yang tidak valid yang masih dideteksi oleh JavaScript, mis .: "\uFFFF"bukan unicode yang valid, jika saya ingat dengan benar, tetapi JS masih dapat membandingkannya ("\uFFFF" === "\uFFFF" = true )

Pada awalnya mungkin terlihat hack-y tetapi, tergantung pada kasus penggunaan Anda, saya tidak melihat masalah besar dengan menggunakan rute ini.

Cara lain untuk mencapai ini

Nah, kita bisa dengan mudah melacak potongan terakhir N(di mana Nsesuai dengan panjang tag multi-panjang terpanjang).

Akan ada beberapa penyesuaian yang harus dilakukan dengan cara loop di dalam metode parseMarkdownberperilaku, yaitu memeriksa apakah potongan saat ini merupakan bagian dari tag multi-panjang, jika itu digunakan sebagai tag; jika tidak, dalam kasus seperti ``k, kita harus menandainya sebagai notMultiLengthatau sesuatu yang serupa dan mendorong potongan itu sebagai konten.

Kode

// Instead of creating hardcoded variables, we can make the code more extendable
// by storing all the possible tags we'll work with in a Map. Thus, creating
// more tags will not require additional logic in our code.
const tags = new Map(Object.entries({
  "*": "strong", // bold
  "!": "button", // action
  "_": "em", // emphasis
  "\uFFFF": "pre", // Just use a very unlikely to happen unicode character,
                   // We'll replace our multi-length symbols with that one.
}));
// Might be useful if we need to discover the symbol of a tag
const tagSymbols = new Map();
tags.forEach((v, k) => { tagSymbols.set(v, k ); })

const rawMarkdown = `
  This must be *bold*,

  This also must be *bo_ld*,

  this _entire block must be
  emphasized even if it's comprised of multiple lines_,

  This is an !action! it should be a button,

  \`\`\`
beep, boop, this is code
  \`\`\`

  This is an asterisk\\*
`;

class App extends React.Component {
  parseMarkdown(source) {
    let currentTag = "";
    let currentContent = "";

    const parsedMarkdown = [];

    // We create this variable to track possible escape characters, eg. "\"
    let before = "";

    const pushContent = (
      content,
      tagValue,
      props,
    ) => {
      let children = undefined;

      // There's the need to parse for empty lines
      if (content.indexOf("\n\n") >= 0) {
        let before = "";
        const contentJSX = [];

        let chunk = "";
        for (let i = 0; i < content.length; i++) {
          if (i !== 0) before = content[i - 1];

          chunk += content[i];

          if (before === "\n" && content[i] === "\n") {
            contentJSX.push(chunk);
            contentJSX.push(<br />);
            chunk = "";
          }

          if (chunk !== "" && i === content.length - 1) {
            contentJSX.push(chunk);
          }
        }

        children = contentJSX;
      } else {
        children = [content];
      }
      parsedMarkdown.push(React.createElement(tagValue, props, children))
    };

    for (let i = 0; i < source.length; i++) {
      const chunk = source[i];
      if (i !== 0) {
        before = source[i - 1];
      }

      // Does our current chunk needs to be treated as a escaped char?
      const escaped = before === "\\";

      // Detect if we need to start/finish parsing our tags

      // We are not parsing anything, however, that could change at current
      // chunk
      if (currentTag === "" && escaped === false) {
        // If our tags array has the chunk, this means a markdown tag has
        // just been found. We'll change our current state to reflect this.
        if (tags.has(chunk)) {
          currentTag = tags.get(chunk);

          // We have simple content to push
          if (currentContent !== "") {
            pushContent(currentContent, "span");
          }

          currentContent = "";
        }
      } else if (currentTag !== "" && escaped === false) {
        // We'll look if we can finish parsing our tag
        if (tags.has(chunk)) {
          const symbolValue = tags.get(chunk);

          // Just because the current chunk is a symbol it doesn't mean we
          // can already finish our currentTag.
          //
          // We'll need to see if the symbol's value corresponds to the
          // value of our currentTag. In case it does, we'll finish parsing it.
          if (symbolValue === currentTag) {
            pushContent(
              currentContent,
              currentTag,
              undefined, // you could pass props here
            );

            currentTag = "";
            currentContent = "";
          }
        }
      }

      // Increment our currentContent
      //
      // Ideally, we don't want our rendered markdown to contain any '\'
      // or undesired '*' or '_' or '!'.
      //
      // Users can still escape '*', '_', '!' by prefixing them with '\'
      if (tags.has(chunk) === false || escaped) {
        if (chunk !== "\\" || escaped) {
          currentContent += chunk;
        }
      }

      // In case an erroneous, i.e. unfinished tag, is present and the we've
      // reached the end of our source (rawMarkdown), we want to make sure
      // all our currentContent is pushed as a simple string
      if (currentContent !== "" && i === source.length - 1) {
        pushContent(
          currentContent,
          "span",
          undefined,
        );
      }
    }

    return parsedMarkdown;
  }

  render() {
    return (
      <div className="App">
        <div>{this.parseMarkdown(this.props.rawMarkdown)}</div>
      </div>
    );
  }
}

ReactDOM.render(<App rawMarkdown={rawMarkdown.replace(/```/g, "\uFFFF")} />, document.getElementById('app'));

Tautan ke kode (TypeScript) https://codepen.io/ludanin/pen/GRgNWPv

Tautan ke kode (vanilla / babel) https://codepen.io/ludanin/pen/eYmBvXw


Saya merasa solusi ini ada di jalur yang benar, tetapi tampaknya ada masalah dengan menempatkan karakter penurunan harga yang lain di dalam yang lain. Misalnya, coba ganti This must be *bold*dengan This must be *bo_ld*. Itu menyebabkan HTML yang dihasilkan menjadi cacat
Ryan Peschel

Kurangnya pengujian yang tepat menghasilkan ini = p, saya buruk. Saya sudah memperbaikinya dan akan memposting hasilnya di sini, sepertinya masalah sederhana untuk diperbaiki.
Lukas Danin

Ya terima kasih. Saya sangat suka solusi ini. Tampaknya sangat kuat dan bersih. Saya pikir itu bisa direactored sedikit meskipun untuk keanggunan lebih. Saya mungkin mencoba mengacaukannya sedikit.
Ryan Peschel

Selesai, omong-omong, saya telah men-tweak kode untuk mendukung cara yang jauh lebih fleksibel untuk mendefinisikan tag penurunan harga dan nilai JSX masing-masing.
Lukas Danin

Hai terima kasih ini terlihat bagus. Hanya satu hal terakhir dan saya pikir itu akan sempurna. Dalam posting asli saya, saya memiliki fungsi untuk cuplikan kode juga (yang melibatkan backticks tiga kali lipat). Apakah mungkin untuk mendapatkan dukungan untuk itu juga? Sehingga tag bisa opsional menjadi beberapa karakter? Balasan lain menambahkan dukungan dengan mengganti instance `` `dengan karakter yang jarang digunakan. Itu akan menjadi cara mudah untuk melakukannya, tetapi tidak yakin apakah itu ideal.
Ryan Peschel

4

Sepertinya Anda mencari solusi kecil yang sangat mendasar. Bukan "monster super" seperti react-markdown-it:)

Saya ingin merekomendasikan Anda https://github.com/developit/snarkdown yang terlihat sangat ringan dan menyenangkan! Hanya 1kb dan sangat sederhana, Anda dapat menggunakannya & memperluasnya jika Anda memerlukan fitur sintaks lainnya.

Daftar tag yang didukung https://github.com/developit/snarkdown/blob/master/src/index.js#L1

Memperbarui

Hanya memperhatikan tentang komponen reaksi, melewatkannya di awal. Jadi itu bagus untuk Anda, saya percaya untuk mengambil perpustakaan sebagai contoh dan mengimplementasikan komponen yang diperlukan khusus Anda untuk menyelesaikannya tanpa menetapkan HTML berbahaya. Perpustakaannya cukup kecil dan jelas. Bersenang-senanglah dengan itu! :)


3
var table = {
  "*":{
    "begin":"<strong>",
    "end":"</strong>"
    },
  "_":{
    "begin":"<em>",
    "end":"</em>"
    },
  "!":{
    "begin":"<MyComponent onClick={this.action}>",
    "end":"</MyComponent>"
    },

  };

var myMarkdown = "hello *asdf* *how* _are_ you !doing! today";
var tagFinder = /(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/gm;

//Use case 1: direct string replacement
var replaced = myMarkdown.replace(tagFinder, replacer);
function replacer(match, whole, tag_begin, content, tag_end, offset, string) {
  return table[tag_begin]["begin"] + content + table[tag_begin]["end"];
}
alert(replaced);

//Use case 2: React components
var pieces = [];
var lastMatchedPosition = 0;
myMarkdown.replace(tagFinder, breaker);
function breaker(match, whole, tag_begin, content, tag_end, offset, string) {
  var piece;
  if (lastMatchedPosition < offset)
  {
    piece = string.substring(lastMatchedPosition, offset);
    pieces.push("\"" + piece + "\"");
  }
  piece = table[tag_begin]["begin"] + content + table[tag_begin]["end"];
  pieces.push(piece);
  lastMatchedPosition = offset + match.length;

}
alert(pieces);

Hasil: Hasil lari

Hasil tes Regexp

Penjelasan:

/(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/
  • Anda dapat menentukan tag Anda di bagian ini [*|!|_]:, setelah salah satu dari mereka dicocokkan, tag itu akan ditangkap sebagai grup dan dinamai sebagai "tag_begin".

  • Dan kemudian (?<content>\w+)menangkap konten yang dibungkus oleh tag.

  • Tag akhir harus sama dengan yang sebelumnya cocok, jadi di sini digunakan \k<tag_begin>, dan jika lulus tes maka tangkap sebagai grup dan beri nama "tag_end", itulah yang (?<tag_end>\k<tag_begin>))dikatakan.

Di JS Anda telah menyiapkan tabel seperti ini:

var table = {
  "*":{
    "begin":"<strong>",
    "end":"</strong>"
    },
  "_":{
    "begin":"<em>",
    "end":"</em>"
    },
  "!":{
    "begin":"<MyComponent onClick={this.action}>",
    "end":"</MyComponent>"
    },

  };

Gunakan tabel ini untuk mengganti tag yang cocok.

Sting.replace memiliki String.replace yang berlebihan (regexp, fungsi) yang dapat mengambil grup yang ditangkap sebagai parameternya, kami menggunakan item yang diambil ini untuk mencari tabel dan menghasilkan string pengganti.

[Perbarui]
Saya telah memperbarui kode, saya menyimpan yang pertama kalau-kalau ada orang lain yang tidak perlu komponen reaksi, dan Anda bisa lihat ada sedikit perbedaan di antara mereka. Bereaksi Komponen


Sayangnya saya tidak yakin apakah ini berhasil. Karena saya membutuhkan komponen Bereaksi aktual dan elemen sendiri, bukan string mereka. Jika Anda melihat di posting asli saya, Anda akan melihat bahwa saya sendiri menambahkan elemen ke array, bukan string. Dan menggunakan hazardouslySetInnerHTML berbahaya karena pengguna dapat memasukkan string berbahaya.
Ryan Peschel

Untungnya ini sangat sederhana untuk mengkonversi penggantian string ke komponen Bereaksi, saya telah memperbarui kode.
Simon

Hm? Saya pasti kehilangan sesuatu, karena mereka masih tergantung pada saya. Saya bahkan membuat biola dengan kode Anda. Jika Anda membaca console.logoutput, Anda akan melihat array penuh dengan string, bukan komponen Bereaksi aktual: jsfiddle.net/xftswh41
Ryan Peschel

Sejujurnya saya tidak tahu Bereaksi, jadi saya tidak bisa membuat semuanya dengan sempurna diikuti oleh kebutuhan Anda, tapi saya pikir informasi tentang bagaimana menyelesaikan pertanyaan Anda sudah cukup, Anda harus memasukkannya ke mesin Bereaksi Anda dan itu hanya bisa pergi.
Simon

Alasan mengapa utas ini ada adalah karena tampaknya secara signifikan lebih sulit untuk menguraikannya menjadi komponen Bereaksi (maka judul utas menentukan kebutuhan yang tepat). Mem-parsing mereka ke dalam string cukup sepele dan Anda bisa menggunakan fungsi ganti string. Senar bukan solusi ideal karena lambat dan rentan terhadap XSS karena harus memanggil dengan berbahayaSetInnerHTML
Ryan Peschel

0

Anda bisa melakukannya seperti ini:

//inside your compoenet

   mapData(myMarkdown){
    return myMarkdown.split(' ').map((w)=>{

        if(w.startsWith('*') && w.endsWith('*') && w.length>=3){
           w=w.substr(1,w.length-2);
           w=<strong>{w}</strong>;
         }else{
             if(w.startsWith('_') && w.endsWith('_') && w.length>=3){
                w=w.substr(1,w.length-2);
                w=<em>{w}</em>;
              }else{
                if(w.startsWith('!') && w.endsWith('!') && w.length>=3){
                w=w.substr(1,w.length-2);
                w=<YourComponent onClick={this.action}>{w}</YourComponent>;
                }
            }
         }
       return w;
    })

}


 render(){
   let content=this.mapData('hello *asdf* *how* _are_ you !doing! today');
    return {content};
  }

0

A working solution purely using Javascript and ReactJs without dangerouslySetInnerHTML.

Pendekatan

Pencarian karakter demi karakter untuk elemen penurunan harga. Segera setelah ditemukan, cari tag penutup untuk hal yang sama dan kemudian ubah menjadi html.

Tag didukung di cuplikan

  • mencolok
  • huruf miring
  • em
  • pra

Input dan Output dari snippet:

JsFiddle: https://jsfiddle.net/sunil12738/wg7emcz1/58/

Kode:

const preTag = "đ"
const map = {
      "*": "b",
      "!": "i",
      "_": "em",
      [preTag]: "pre"
    }

class App extends React.Component {
    constructor(){
      super()
      this.getData = this.getData.bind(this)
    }

    state = {
      data: []
    }
    getData() {
      let str = document.getElementById("ta1").value
      //If any tag contains more than one char, replace it with some char which is less frequently used and use it
      str = str.replace(/```/gi, preTag)
      const tempArr = []
      const tagsArr = Object.keys(map)
      let strIndexOf = 0;
      for (let i = 0; i < str.length; ++i) {
        strIndexOf = tagsArr.indexOf(str[i])
        if (strIndexOf >= 0 && str[i-1] !== "\\") {
          tempArr.push(str.substring(0, i).split("\\").join("").split(preTag).join(""))
          str = str.substr(i + 1);
          i = 0;
          for (let j = 0; j < str.length; ++j) {
            strIndexOf = tagsArr.indexOf(str[j])
            if (strIndexOf >= 0 && str[j-1] !== "\\") {
              const Tag = map[str[j]];
              tempArr.push(<Tag>{str.substring(0, j).split("\\").join("")}</Tag>)
              str = str.substr(j + 1);
              i = 0;
              break
             }
          }
        }
      }
      tempArr.push(str.split("\\").join(""))
      this.setState({
        data: tempArr,
      })
    }
    render() {
      return (
        <div>
          <textarea rows = "10"
            cols = "40"
           id = "ta1"
          /><br/>
          <button onClick={this.getData}>Render it</button><br/> 
          {this.state.data.map(x => x)} 
        </div>
      )
    }
  }

ReactDOM.render(
  <App/>,
  document.getElementById('root')
);
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
  <div id="root"></div>
</body>

Penjelasan terperinci (dengan contoh):

Misalkan jika string adalah How are *you* doing? Jauhkan pemetaan untuk simbol untuk tag

map = {
 "*": "b"
}
  • Ulangi sampai Anda menemukan pertama *, teks sebelum itu adalah string normal
  • Dorong bagian dalam array itu. Array menjadi ["How are "]dan mulai loop dalam sampai Anda menemukan * berikutnya.
  • Now next between * and * needs to be bold, kami mengonversinya dalam elemen html dengan teks dan langsung mendorong dalam array di mana Tag = b dari peta. Jika Anda melakukannya <Tag>text</Tag>, reaksi konversi internal menjadi teks dan dorong ke array. Sekarang array adalah ["apa kabar", Anda ]. Istirahat dari lingkaran dalam
  • Sekarang kita mulai loop luar dari sana dan tidak ada tag yang ditemukan, jadi dorong sisa dalam array. Array menjadi: ["apa kabar", Anda , "melakukan"].
  • Render di UI How are <b>you</b> doing?
    Note: <b>you</b> is html and not text

Catatan : Bersarang juga dimungkinkan. Kita perlu memanggil logika di atas dalam rekursi

Untuk menambahkan dukungan Tag baru

  • Jika mereka satu karakter seperti * atau!, Tambahkan mereka di mapobjek dengan kunci sebagai karakter dan nilai sebagai tag yang sesuai
  • Jika mereka lebih dari satu karakter seperti `` `, buat peta satu ke satu dengan beberapa karakter yang jarang digunakan dan kemudian masukkan (Alasan: saat ini, pendekatan berdasarkan karakter oleh pencarian karakter dan lebih dari satu karakter akan rusak. Namun , yang juga dapat dijaga dengan meningkatkan logika)

Apakah ini mendukung bersarang? Tidak
Apakah ini mendukung semua kasus penggunaan yang disebutkan oleh OP? Iya

Semoga ini bisa membantu.


Hai, lihat ini sekarang. Apakah ini mungkin untuk digunakan dengan dukungan triple backtick juga? Jadi `` `asdf``` akan berfungsi dengan baik untuk blok kode?
Ryan Peschel

Akan tetapi beberapa modifikasi mungkin diperlukan. Saat ini, hanya pencocokan karakter tunggal untuk * atau! Itu perlu dimodifikasi sedikit. Pada dasarnya blok kode berarti asdfakan dirender <pre>asdf</pre>dengan latar belakang gelap, bukan? Biarkan saya tahu ini dan saya akan melihat. Bahkan Anda bisa mencobanya sekarang. Pendekatan sederhana adalah: Dalam solusi di atas, ganti `` `dalam teks dengan karakter khusus seperti ^ atau ~ dan petakan ke pra tag. Maka itu akan bekerja dengan baik. Pendekatan lain membutuhkan beberapa pekerjaan lagi
Sunil Chaudhary

Ya, tepatnya, mengganti `` `asdf``` dengan <pre>asdf</pre>. Terima kasih!
Ryan Peschel

@RyanPeschel Hai! Telah menambahkan predukungan tag juga. Beritahu saya jika berhasil
Sunil Chaudhary

Solusi menarik (menggunakan karakter langka). Satu masalah yang masih saya lihat adalah kurangnya dukungan untuk melarikan diri (sehingga \ * asdf * tidak dicetak tebal), yang saya sertakan dukungan dalam kode di pos asli saya (juga disebutkan dalam elaborasi tertaut saya di akhir pos). Apakah itu sangat sulit untuk ditambahkan?
Ryan Peschel
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.