Jak parsować mały podzbiór Markdown w komponenty React?


9

Mam bardzo mały podzbiór Markdown wraz z niestandardowym plikiem HTML, który chciałbym przeanalizować w komponentach React. Na przykład chciałbym włączyć następujący ciąg:

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

Do następującej tablicy:

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

a następnie zwróć go z funkcji renderowania React (React poprawnie wyrenderuje tablicę jako sformatowany HTML)

Zasadniczo chcę dać użytkownikom możliwość użycia bardzo ograniczonego zestawu Markdown do zamiany tekstu na komponenty w stylu (a w niektórych przypadkach na własne!)

Niebezpiecznie jestSetInnerHTML, i nie chcę wprowadzać zewnętrznej zależności, ponieważ wszystkie są bardzo ciężkie i potrzebuję tylko bardzo podstawowej funkcjonalności.

Obecnie robię coś takiego, ale jest to bardzo kruche i nie działa we wszystkich przypadkach. Zastanawiałem się, czy istnieje lepszy sposób:

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;
}

Oto moje poprzednie pytanie, które doprowadziło do tego.


1
Co jeśli wejście zawiera zagnieżdżone elementy, takie jak font _italic *and bold* then only italic_ and normal? Jaki byłby oczekiwany wynik? Czy nigdy nie będzie zagnieżdżony?
trincot,

1
Nie musisz się martwić o zagnieżdżanie. Jest to po prostu bardzo proste zniżki dla użytkowników. Wszystko, co jest najłatwiejsze do wdrożenia, jest dla mnie w porządku. W twoim przykładzie byłoby całkowicie dobrze, gdyby wewnętrzne pogrubienie nie działało. Ale jeśli łatwiej jest zaimplementować zagnieżdżanie, niż go nie mieć, to też jest w porządku.
Ryan Peschel,

1
Prawdopodobnie najłatwiej jest po prostu skorzystać z gotowego rozwiązania, takiego jak npmjs.com/package/react-markdown-it
mb21

1
Jednak nie używam przecen. Jest to po prostu bardzo podobny / mały jego podzbiór (który obsługuje kilka niestandardowych komponentów, wraz z nie zagnieżdżonymi pogrubieniem, kursywą, kodem, podkreśleniem). Fragmenty, które opublikowałem, działają nieco, ale nie wydają się zbyt idealne i zawodzą w niektórych trywialnych przypadkach (np. Nie można wpisać jednej gwiazdki w ten sposób: asdf*bez znikania)
Ryan Peschel

1
cóż ... parsowanie Markdown lub coś takiego jak Markdown nie jest łatwym zadaniem ... Wyrażenia regularne nie odcinają go ... podobne pytanie dotyczące HTML, patrz stackoverflow.com/questions/1732348/…
mb21

Odpowiedzi:


1

Jak to działa?

Działa poprzez czytanie fragmentu łańcucha po kawałku, co może nie być najlepszym rozwiązaniem dla naprawdę długich łańcuchów.

Ilekroć parser wykryje, że krytyczny fragment jest czytany, tj. '*'Lub jakikolwiek inny znacznik, zaczyna analizować fragmenty tego elementu, aż parser znajdzie znacznik zamykający.

Działa na ciągach wieloliniowych, patrz na przykład kod.

Ostrzeżenia

Nie określiłeś, lub mógłbym źle zrozumieć twoje potrzeby, jeśli istnieje konieczność parsowania tagów pogrubionych i kursywnych , moje obecne rozwiązanie może nie działać w tym przypadku.

Jeśli jednak potrzebujesz pracować z powyższymi warunkami, po prostu skomentuj tutaj, a ja poprawię kod.

Pierwsza aktualizacja: poprawia sposób traktowania znaczników przeceny

Tagi nie są już zakodowane na stałe, ale są mapą, na której można łatwo rozszerzyć w celu dopasowania do swoich potrzeb.

Naprawiono błędy, o których wspomniałeś w komentarzach, dzięki za wskazanie tych problemów = p

Druga aktualizacja: znaczniki przeceny o wielu długościach

Najłatwiejszy sposób na osiągnięcie tego: zastąpienie znaków o wielu długościach rzadko używanym Unicode

Chociaż metoda parseMarkdownnie obsługuje jeszcze tagów o wielu długościach, możemy z łatwością zastąpić te znaczniki o dużej długości prostymi string.replace podczas wysyłania naszego rawMarkdownrekwizytu.

Aby zobaczyć przykład tego w praktyce, spójrz na ReactDOM.render, znajdujący się na końcu kodu.

Nawet jeśli aplikacja nie obsługuje wiele języków, są nieprawidłowe znaki Unicode że JavaScript nadal wykrywa, np .:"\uFFFF" nie jest poprawnym Unicode, jeśli dobrze pamiętam, ale JS nadal będzie mógł je porównać ( "\uFFFF" === "\uFFFF" = true)

Na początku może wydawać się hack-y, ale w zależności od przypadku użycia nie widzę większych problemów z korzystaniem z tej trasy.

Kolejny sposób na osiągnięcie tego

Cóż, możemy łatwo śledzić ostatnie N(gdzieN odpowiada długości najdłuższego tagu o wielu długościach).

Należy wprowadzić pewne poprawki w sposobie, w jaki parseMarkdownzachowuje się metoda wewnątrz pętli , tj. Sprawdzanie, czy bieżący fragment jest częścią znacznika o wielu długościach, jeśli jest on używany jako znacznik; w przeciwnym razie ``kmusielibyśmy to oznaczyć jakonotMultiLength coś podobnego i przesunąć ten fragment jako treść.

Kod

// 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'));

Link do kodu (TypeScript) https://codepen.io/ludanin/pen/GRgNWPv

Link do kodu (waniliowy / babel) https://codepen.io/ludanin/pen/eYmBvXw


Wydaje mi się, że to rozwiązanie jest na dobrej drodze, ale wydaje się, że ma problemy z umieszczaniem innych znaków przeceny w innych. Na przykład, spróbuj wymienić This must be *bold*z This must be *bo_ld*. Powoduje zniekształcenie wynikowego kodu HTML
Ryan Peschel,

Brak odpowiednich testów spowodował to = p, mój zły. Już to naprawiam i opublikuję tutaj wynik, wydaje się prostym problemem do naprawienia.
Lukas Danin

Tak dzieki. Naprawdę podoba mi się to rozwiązanie. Wydaje się bardzo solidny i czysty. Myślę, że można go nieco przebudować, aby uzyskać jeszcze więcej elegancji. Może spróbuję trochę z tym pogodzić.
Ryan Peschel,

Przy okazji, poprawiłem kod, aby obsługiwał znacznie bardziej elastyczny sposób definiowania znaczników i ich odpowiednich wartości JSX.
Lukas Danin

Hej, dzięki, wygląda świetnie. Jeszcze jedna rzecz i myślę, że będzie idealnie. W moim oryginalnym poście mam również funkcję dla fragmentów kodu (które obejmują potrójne wsteczne). Czy możliwe byłoby również wsparcie tego? Czyli tagi mogą opcjonalnie składać się z wielu znaków? Inna odpowiedź dodała wsparcie, zastępując wystąpienia `` 'rzadko używaną postacią. Byłby to łatwy sposób, ale nie jestem pewien, czy to jest idealne.
Ryan Peschel,

4

Wygląda na to, że szukasz małego, bardzo podstawowego rozwiązania. Nie takie „super-potwory” jak react-markdown-it:)

Chciałbym polecić Ci https://github.com/developit/snarkdown który wygląda dość lekki i ładny! Tylko 1kb i niezwykle prosty, możesz go użyć i rozszerzyć, jeśli potrzebujesz innych funkcji składni.

Lista obsługiwanych tagów https://github.com/developit/snarkdown/blob/master/src/index.js#L1

Aktualizacja

Zauważyłem, że reagują komponenty, na początku tego nie zauważyłem. To dla ciebie świetne. Uważam, że możesz wziąć bibliotekę jako przykład i zaimplementować niestandardowe wymagane komponenty, aby zrobić to bez niebezpiecznego ustawiania HTML. Biblioteka jest dość mała i przejrzysta. Baw się dobrze! :)


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);

Wynik: Uruchomiony wynik

Wynik testu Regexp

Wyjaśnienie:

/(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/
  • Możesz zdefiniować tagi w tej sekcji: [*|!|_]po dopasowaniu jednego z nich zostanie on przechwycony jako grupa i nazwany „tag_begin”.

  • A następnie (?<content>\w+)przechwytuje zawartość zawiniętą przez tag.

  • Znacznik końcowy musi być taki sam jak poprzednio dopasowany, więc tutaj używa \k<tag_begin>, a jeśli pomyślnie przejdzie test, przechwyć go jako grupę i nadaj mu nazwę „tag_end”, tak właśnie (?<tag_end>\k<tag_begin>))jest.

W JS utworzyłeś tabelę taką:

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

  };

Użyj tej tabeli, aby zastąpić dopasowane tagi.

Sting.replace ma przeciążenie String.replace (regexp, funkcja) która może przyjmować przechwycone grupy jako parametry, używamy tych przechwyconych elementów do wyszukiwania tabeli i generowania zastępującego łańcucha.

[Aktualizacja] Zaktualizowałem
kod, zachowałem pierwszy, na wypadek, gdyby ktoś nie potrzebował reagować na komponenty i widać, że między nimi jest niewielka różnica. Reaguj komponenty


Niestety nie jestem pewien, czy to działa. Ponieważ potrzebuję samych komponentów React i samych elementów, a nie ich ciągów. Jeśli spojrzysz na mój oryginalny post, zobaczysz, że dodam same elementy do tablicy, a nie ich ciągi. Korzystanie z niebezpiecznieSetInnerHTML jest niebezpieczne, ponieważ użytkownik może wprowadzić złośliwe ciągi.
Ryan Peschel,

Na szczęście bardzo łatwo przekonwertować zamianę łańcucha na komponenty React, zaktualizowałem kod.
Simon

Hm? Muszę coś przeoczyć, ponieważ wciąż są łańcuchami po mojej stronie. Zrobiłem nawet skrzypce z twoim kodem. Jeśli przeczytasz console.logwynik, zobaczysz, że tablica jest pełna ciągów, a nie rzeczywistych składników React: jsfiddle.net/xftswh41
Ryan Peschel

Szczerze mówiąc, nie znam React, więc nie mogę sprawić, aby wszystko było idealnie dostosowane do twoich potrzeb, ale myślę, że informacje o tym, jak rozwiązać twoje pytanie, są wystarczające, musisz umieścić je na swojej maszynie React i po prostu może odejść.
Simon

Powodem, dla którego istnieje ten wątek, jest fakt, że parsowanie go w komponenty React wydaje się znacznie trudniejsze (stąd tytuł wątku określa dokładnie tę potrzebę). Przetwarzanie ich w łańcuchy jest dość trywialne i możesz po prostu użyć funkcji zamiany łańcucha. Ciągi nie są idealnym rozwiązaniem, ponieważ są powolne i podatne na XSS ze względu na konieczność wywoływania niebezpiecznieSetInnerHTML
Ryan Peschel

0

możesz to zrobić w następujący sposób:

//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.

Podejście

Wyszukiwanie znak po znaku dla elementów przecenionych. Jak tylko zostanie napotkany, wyszukaj tag końcowy dla tego samego, a następnie przekonwertuj go na HTML.

Tagi obsługiwane we fragmencie

  • pogrubienie
  • kursywa
  • em
  • przed

Dane wejściowe i wyjściowe z fragmentu:

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

Kod:

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>

Szczegółowe wyjaśnienie (z przykładem):

Załóżmy, że jeśli ciąg znaków to How are *you* doing? Zachowaj mapowanie symboli na tagi

map = {
 "*": "b"
}
  • Pętla, aż znajdziesz pierwszy *, przed nim tekst jest normalnym ciągiem
  • Wepchnij to do tablicy. Tablica staje się ["How are "]i rozpoczyna wewnętrzną pętlę, aż znajdziesz następną *.
  • Now next between * and * needs to be bold, konwertujemy je w elemencie HTML tekstem i bezpośrednio pchamy tablicę, w której Tag = b z mapy. Jeśli tak <Tag>text</Tag>, reaguj wewnętrznie konwertuje na tekst i wypychaj do tablicy. Teraz tablica to [jak się masz , ty ]. Zerwij z wewnętrznej pętli
  • Teraz zaczynamy stamtąd zewnętrzną pętlę i nie znaleziono znaczników, więc wciśnij pozostając w tablicy. Tablica staje się: [„How are”, you , „doing”].
  • Renderuj w interfejsie użytkownika How are <b>you</b> doing?
    Note: <b>you</b> is html and not text

Uwaga : Zagnieżdżanie jest również możliwe. Musimy nazwać powyższą logikę rekurencją

Aby dodać obsługę nowych tagów

  • Jeśli są one jednym znakiem, takim jak * lub!, Dodaj je w mapobiekcie z kluczem jako znakiem i wartością jako odpowiednim znacznikiem
  • Jeśli są więcej niż jedną postacią, np. `` '', Utwórz mapę jeden do jednego za pomocą rzadziej używanego znaku, a następnie wstaw (Powód: obecnie podejście oparte na wyszukiwaniu znaków po znaku i więcej niż jeden znak ulegnie awarii. Jednak , można to również rozwiązać poprzez poprawę logiki)

Czy obsługuje zagnieżdżanie? Nie
Czy obsługuje wszystkie przypadki użycia wymienione przez OP? tak

Mam nadzieję, że to pomoże.


Cześć, przeglądam to teraz. Czy jest to również możliwe w przypadku potrójnego wsparcia wstecznego? Więc `` asdf`` działałby również dla bloków kodu?
Ryan Peschel,

Będzie, ale mogą być potrzebne pewne modyfikacje. Obecnie istnieje tylko dopasowanie pojedynczych znaków dla * lub! To trzeba trochę zmodyfikować. Bloki kodu w zasadzie oznaczają, asdfże będą renderowane na <pre>asdf</pre>ciemnym tle, prawda? Daj mi znać, a zobaczę. Nawet ty możesz spróbować teraz. Proste podejście: w powyższym rozwiązaniu zamień `` `w tekście na znak specjalny, taki jak ^ lub ~, i zamapuj go na tag wstępny. To będzie działać dobrze. Inne podejście wymaga nieco więcej pracy
Sunil Chaudhary

Tak, dokładnie, zastępując `` asdf`` przez <pre>asdf</pre>. Dzięki!
Ryan Peschel,

@RyanPeschel Cześć! Dodałem także preobsługę tagów. Daj mi znać, jeśli to
zadziała

Ciekawe rozwiązanie (użycie rzadkiej postaci). Jedną z kwestii, które wciąż widzę, jest brak wsparcia dla ucieczki (takie, że \ * asdf * nie jest pogrubiony), co włączyłem wsparcie w kodzie w moim oryginalnym poście (wspomniałem o tym również w moim linkowanym opracowaniu na końcu Poczta). Czy byłoby to bardzo trudne do dodania?
Ryan Peschel,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.