Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

Biblioteka TreeStructInfo JavaScript

Logo TreeStructInfo

Wprowadzenie

TreeStructInfoJS to lekka, bezzewnętrzna biblioteka JavaScript umożliwiająca przetwarzanie danych zapisanych w formacie TreeStructInfo 2.0 – czytelnej, drzewiastej strukturze tekstowej opartej na atrybutach i węzłach.

Zawiera dwie główne klasy:

  • TreeStructInfoParserJS – parser tekstu .tsinfo do struktury json ({ version, name, data })
  • TreeStructInfoExporterJS – eksporter odwrotny: ze struktury do formatu .tsinfo

Przeznaczona do pracy w przeglądarce, biblioteka może być wykorzystywana np. do importu/eksportu konfiguracji, danych hierarchicznych, zasobów tekstowych, itp.

Struktura danych TreeStructInfo

Przykład reprezentacji:

treestructinfo "2.0" name "Example"
  attr title "Hello"
  attr count "123"
  node meta    
    attr enabled "true"
  end node
  ref attr secret
end tree
ref attr secret "token-xyz"
Ważne
  • w pliku konfiguracyjnym musi być zachowana odpowiednia kolejność definiowania danych
  • atrybuty i węzły referencjonowane muszą być zdefiniowane po całym drzewie z danymi czyli po end tree
  • atrybuty referencjonowane muszą być zdefiniowane przed węzłami referencjonowanymi
  • węzły referencjonowane występujące w innych węzłach referencjonowanych muszą być zdefiniowane przed nimi
  • przy włączonym automatycznym rzutowaniu typów separatorem miejsc dziesiętnych musi być kropka

Parser zamienia powyższy zapis na:

{
  "version": "2.0",
  "name": "Example",
  "data": {
    "title": "Hello",
    "count": 123,
    "meta": {
      "enabled": true
    },
    "secret": "token-xyz"
  }
}

TreeStructInfoParserJS

Parser wczytuje dane z formatu .tsinfo i konwertuje je do struktury obiektowej. Obsługuje:

  • automatyczne rzutowanie typów (int, float, bool, base64, hex)
  • węzły node ... end node
  • atrybuty attr, również wielolinijkowe
  • atrybuty referencyjne ref attr ... (lokalne lub zdefiniowane wcześniej)
  • komentarze ::

Kod parsera:

/**
 * TreeStructInfoParserJS
 *
 * A parser for the TreeStructInfo textual format.
 * It parses the versioned header, hierarchical nodes, attributes,
 * reference attributes, and supports multi-line values and auto-casting.
 * Copyright (c) 2025 Dariusz Rorat
 * Licensed under the BSD 3-Clause License.
 */
class TreeStructInfoParserJS {
  constructor() {
    /**
     * @private
     * @type {string[]}
     */
    this.lines = [];

    /**
     * @private
     * @type {string[]}
     */
    this.refLines = [];

    /**
     * @private
     * @type {number}
     */
    this.currentLine = 0;

    /**
     * @private
     * @type {number}
     */
    this.currentRefLine = 0;

    /**
     * Stores reference attributes encountered during parsing.
     * @type {Object.<string, string>}
     */
    this.references = {};

    /**
     * Stores node reference attributes encountered during parsing.
     * @type {Object.<string, string>}
     */
    this.nodeReferences = {};

    /**
     * Whether to auto-cast values (e.g. booleans, numbers).
     * @type {boolean}
     */
    this.autoCasting = false;

    /**
     * Character used to join multi-line values.
     * @type {string}
     */
    this.multilineJoinChar = "\n";

    /**
     * List of string values recognized as boolean `true`.
     * @type {string[]}
     */
    this.trueValues = ['true'];

    /**
     * List of string values recognized as boolean `false`.
     * @type {string[]}
     */
    this.falseValues = ['false'];
  }

  /**
   * Enables or disables automatic casting of values.
   * @param {boolean} value
   */
  setAutoCasting(value) {
    this.autoCasting = value;
  }

  /**
   * Sets the character used to join multi-line string values.
   * @param {string} char
   */
  setMultilineJoinChar(char) {
    this.multilineJoinChar = char;
  }

  /**
   * Sets the values that should be recognized as booleans.
   * @param {string[]} trueValues
   * @param {string[]} falseValues
   */
  setBoolValues(trueValues, falseValues) {
    if (!trueValues.length || !falseValues.length) {
      throw new Error("Empty boolean value list");
    }
    this.trueValues = trueValues;
    this.falseValues = falseValues;
  }

  /**
   * Parses the TreeStructInfo string into a JavaScript object.
   * @param {string} input
   * @returns {{version: string, name: string|null, data: Object}}
   */
parse(input) {
  const allLines = input
    .split('\n')
    .map(line => line.trim())
    .filter(line => line.length > 0);

  const treeLines = [];
  let i = 0;
  let lastRefAttrName = null;
  let endTree = false;

  this.refLines = [];
  this.currentRefLine = 0;

  while (i < allLines.length) {
    const line = allLines[i];

    // Match: ref attr Name "Value"
    const refAttrMatch = line.match(/^ref attr (.+?)\s+"([^"]+)"$/);
    if (refAttrMatch) {
      const [ , name, value ] = refAttrMatch;
      this.references[name] = value;
      lastRefAttrName = name;
      i++;
      continue;
    }

    // Multiline continuation of last ref attr
    if (lastRefAttrName !== null && line.startsWith('"')) {
      this.references[lastRefAttrName] += this.multilineJoinChar + line.replace(/^"+|"+$/g, '');
      i++;
      continue;
    }

    if (line.startsWith('end tree')) {
      endTree = true;
    }

    // Match: ref node SomeName
    const refNodeMatch = line.match(/^ref node (.+?)$/);
    if (endTree && refNodeMatch) {
      const refName = refNodeMatch[1];
      const nodeName = `node ${refName}`;
      this.refLines.push(nodeName);
      i++; // Skip the "ref node ..." line

      // Collect ref node lines until "end ref node"
      while (i < allLines.length && allLines[i].trim() !== 'end ref node') {
        this.refLines.push(allLines[i]);
        i++;
      }

      this.refLines.push('end node');

      if (!allLines[i] || allLines[i].trim() !== 'end ref node') {
        throw new Error(`Missing 'end ref node' for ref node ${refName}`);
      }

      i++; // Skip 'end ref node'

      const refNode = this.parseAnonymousNode();
      this.nodeReferences[refName] = refNode[refName];
      continue;
    }

    // Regular line goes into main tree
    treeLines.push(line);
    i++;
  }

  this.lines = treeLines;
  this.currentLine = 0;

  return this.parseTree();
}

    /**
     * Parses a reference node structure (with optional children).
     *
     */
parseAnonymousNode() {
  const line = this.nextRefLine();

  const match = line.match(/^node\s+(.+?)$/);
  if (!match) {
    throw new Error(`Invalid node start: ${line}`);
  }

  const name = match[1];
  const node = {};
  node[name] = {};
  let lastname = null;

  while ((this.peekRefLine() !== null) && this.peekRefLine() !== 'end node') {
    const currentLine = this.peekRefLine();

    if (currentLine.startsWith('::')) {
      this.nextRefLine(); // skip comment
    } else if (currentLine.startsWith('attr ')) {
      const attr = this.parseAttr(this.nextRefLine());
      node[name][attr.name] = attr.value;
      lastname = attr.name;
    } else if (currentLine.startsWith('"')) {
      const attrLine = this.nextRefLine();
      if (lastname) {
        node[name][lastname] += this.multilineJoinChar + attrLine.replace(/^"+|"+$/g, '');
      }
    } else if (currentLine.startsWith('ref attr ')) {
      const attr = this.resolveRefAttr(this.nextRefLine());
      node[name][attr.name] = attr.value;
      lastname = attr.name;
    } else if (currentLine.startsWith('ref node ')) {
      const refNode = this.resolveRefNode(this.nextRefLine());
      const childName = refNode.name;
      const childValue = refNode.value;
      node[name][childName] = childValue;
    } else if (currentLine.startsWith('node ')) {
      const childNode = this.parseAnonymousNode();
      if (Array.isArray(node[name])) {
        node[name].push(childNode);
      } else if (typeof node[name] === 'object') {
        node[name] = {
          ...node[name],
          ...childNode
        };
      } else {
        node[name] = childNode;
      }
    } else {
      throw new Error(`Unexpected line in node: ${currentLine}`);
    }
  }

  this.expectRefLine('end node');
  return node;
}

  /**
   * @private
   * Parses the main tree structure.
   */
parseTree() {
  let header = this.nextLine();

  while (header.startsWith('::')) {
    header = this.nextLine(); // skip comments
  }

  const match = header.match(/^treestructinfo\s+"(\d+\.\d+)"(?:\s+name\s+"([^"]+)")?$/);
  if (!match) {
    throw new Error("Invalid header format");
  }

  const result = {
    version: match[1],
    name: match[2] || null
  };

  const tree = {};
  let lastname = null;

  while (this.peekLine() !== null && this.peekLine() !== 'end tree') {
    const line = this.peekLine();

    if (line.startsWith('::')) {
      this.nextLine(); // skip comment
    } else if (line.startsWith('attr ')) {
      const attr = this.parseAttr(this.nextLine());
      tree[attr.name] = attr.value;
      lastname = attr.name;
    } else if (line.startsWith('"')) {
      const continuation = this.nextLine();
      if (lastname) {
        tree[lastname] += this.multilineJoinChar + continuation.replace(/^"+|"+$/g, '');
      }
    } else if (line.startsWith('ref attr ')) {
      const attr = this.resolveRefAttr(this.nextLine());
      tree[attr.name] = attr.value;
      lastname = attr.name;
    } else if (line.startsWith('ref node ')) {
      const refNode = this.resolveRefNode(this.nextLine());
      tree[refNode.name] = refNode.value;
    } else if (line.startsWith('node ')) {
      const node = this.parseNode();
      Object.assign(tree, node); // equivalent of array_merge in PHP
    } else {
      throw new Error(`Unexpected line in tree: ${line}`);
    }
  }

  this.expectLine('end tree');
  result.data = tree;

  return result;
}

  /**
   * @private
   * Parses a single node.
   * @returns {Object}
   */
parseNode() {
  const line = this.nextLine();

  const match = line.match(/^node\s+(.+?)$/);
  if (!match) {
    throw new Error(`Invalid node start: ${line}`);
  }

  const name = match[1];
  const node = {};
  node[name] = {};
  let lastname = null;

  while (this.peekLine() !== null && this.peekLine() !== 'end node') {
    const currentLine = this.peekLine();

    if (currentLine.startsWith('::')) {
      this.nextLine(); // skip comment
    } else if (currentLine.startsWith('attr ')) {
      const attr = this.parseAttr(this.nextLine());
      node[name][attr.name] = attr.value;
      lastname = attr.name;
    } else if (currentLine.startsWith('"')) {
      const continuation = this.nextLine();
      if (lastname) {
        node[name][lastname] += this.multilineJoinChar + continuation.replace(/^"+|"+$/g, '');
      }
    } else if (currentLine.startsWith('ref attr ')) {
      const attr = this.resolveRefAttr(this.nextLine());
      node[name][attr.name] = attr.value;
      lastname = attr.name;
    } else if (currentLine.startsWith('ref node ')) {
      const refNode = this.resolveRefNode(this.nextLine());
      node[name][refNode.name] = refNode.value;
    } else if (currentLine.startsWith('node ')) {
      const childNode = this.parseNode();
      if (typeof node[name] === 'object' && !Array.isArray(node[name])) {
        Object.assign(node[name], childNode);
      } else {
        node[name] = childNode;
      }
    } else {
      throw new Error(`Unexpected line in node: ${currentLine}`);
    }
  }

  this.expectLine('end node');
  return node;
}

  /**
   * @private
   * Parses an attribute line.
   * @param {string} line
   * @returns {{name: string, value: any}}
   */
  parseAttr(line) {
    const match = line.match(/^attr\s+(.+?)\s+"([^"]*)"?$/);
    if (!match) {
      throw new Error("Invalid attribute: " + line);
    }

    const name = match[1];
    const rawValue = match[2] || '';
    const value = this.autoCasting ? this.autoCast(rawValue) : rawValue;

    return { name, value };
  }

  /**
   * @private
   * Resolves a reference attribute line.
   * @param {string} line
   * @returns {{name: string, value: any}}
   */
  resolveRefAttr(line) {
    const match = line.match(/^ref attr (.+)$/);
    if (!match) {
      throw new Error("Invalid ref attr: " + line);
    }

    const name = match[1];
    if (!(name in this.references)) {
      throw new Error("Undefined reference: " + name);
    }

    const value = this.references[name];
    return {
      name,
      value: this.autoCasting ? this.autoCast(value) : value
    };
  }

  /**
   * @private
   * Resolves a reference node.
   * @param {string} line
   * @returns {{name: string, value: any}}
   */
  resolveRefNode(line) {
    const match = line.match(/^ref node (.+)$/);
    if (!match) {
      throw new Error("Invalid node usage: " + line);
    }

    const name = match[1];
    if (!(name in this.nodeReferences)) {
      throw new Error("Undefined reference: " + name);
    }

    const value = this.nodeReferences[name];
    return {
      name,
      value: value
    };
  }

  /**
   * @private
   * Peeks at the current line without advancing the cursor.
   * @returns {string|null}
   */
  peekLine() {
    return this.lines[this.currentLine] || null;
  }

  /**
   * @private
   * Peeks at the current ref line without advancing the cursor.
   * @returns {string|null}
   */
  peekRefLine() {
    return this.refLines[this.currentRefLine] || null;
  }

  /**
   * @private
   * Gets the current line and advances the cursor.
   * @returns {string|null}
   */
  nextLine() {
    return this.lines[this.currentLine++] || null;
  }

  /**
   * @private
   * Gets the current ref line and advances the cursor.
   * @returns {string|null}
   */
  nextRefLine() {
    return this.refLines[this.currentRefLine++] || null;
  }

  /**
   * @private
   * Ensures the next line matches the expected value.
   * @param {string} expected
   */
  expectLine(expected) {
    const line = this.nextLine();
    if (line !== expected) {
      throw new Error(`Expected "${expected}" but found "${line}"`);
    }
  }

  /**
   * @private
   * Ensures the next ref line matches the expected value.
   * @param {string} expected
   */
  expectRefLine(expected) {
    const line = this.nextRefLine();
    if (line !== expected) {
      throw new Error(`Expected "${expected}" but found "${line}"`);
    }
  }

  /**
   * @private
   * Attempts to convert a string value to boolean, number, base64, etc.
   * @param {string} value
   * @returns {any}
   */
  autoCast(value) {
        const trimmed = value.trim();

        // "X,Y" Coords
        if (/^\+?(0[xob])?[0-9a-fA-F]+,\+?(0[xob])?[0-9a-fA-F]+$/i.test(trimmed)) {
            return trimmed.split(',').map(v => this.autoCast(v));
        }

        const lower = trimmed.toLowerCase();
        if (this.trueValues.includes(lower)) return true;
        if (this.falseValues.includes(lower)) return false;

        // Hex (np. 0xA3)
        if (/^0x[0-9a-f]+$/i.test(trimmed)) return parseInt(trimmed, 16);

        // Binary (np. 0b1010)
        if (/^0b[01]+$/i.test(trimmed)) return parseInt(trimmed.slice(2), 2);

        // Octal (np. 0o755)
        if (/^0o[0-7]+$/i.test(trimmed)) return parseInt(trimmed.slice(2), 8);

        // Integer
        if (/^[+-]?\d+$/.test(trimmed)) return parseInt(trimmed, 10);

        // Float
        if (/^[+-]?\d+[.,]\d+([eE][+-]?\d+)?$/.test(trimmed)) {
            return parseFloat(trimmed.replace(',', '.'));
        }

        // Base64
        try {
            if (btoa(atob(trimmed)) === trimmed) {
                return atob(trimmed);
            }
        } catch {
            // Not valid base64
        }

        return trimmed;
    }

}

Przykład użycia parsera:

const parser = new TreeStructInfoParserJS();
parser.setAutoCasting(true);

const result = parser.parse(tsinfoText);
console.log(result.version);  // "2.0"
console.log(result.data.title);  // np. "Hello"
Ostrzeżenie

Automatyczne rzutowanie może powodować nieprawidłową interpretację danych. Jeśli wartości float będą miały przecinek jako miejsce dziesiętne, zostanie to zinterpretowane jako współrzędne. Niektóre atrybuty mogą także zwracać null.

TreeStructInfoExporterJS

Eksporter działa odwrotnie: przyjmuje obiekt { version, name, data } i generuje tekst .tsinfo. Obsługuje:

  • struktury zagnieżdżone
  • różne typy danych
  • ręczne definiowanie referencji (ref attr), (ref node)
  • opcjonalne formatowanie wcięć

Kod eksportera

/**
 * TreeStructInfoExporterJS
 *
 * Serializes a structured JavaScript object into the TreeStructInfo format.
 * Supports indentation, quoting, and reference attribute declarations.
 * Copyright (c) 2025 Dariusz Rorat
 * Licensed under the BSD 3-Clause License.
 */
class TreeStructInfoExporterJS {
  constructor() {
    /**
     * String used for indentation (default: two spaces).
     * @type {string}
     */
    this.indent = "  ";

    /**
     * Whether to automatically quote string values (default: true).
     * Currently unused internally but can be wired to control behavior.
     * @type {boolean}
     */
    this.autoQuoting = true;

    /**
     * Reference attributes to be declared at the end of the export.
     * @type {Object.<string, string>}
     */
    this.refAttributes = {};
  }

  /**
   * Sets the indentation string used for nested nodes.
   * @param {string} indentString
   */
  setIndent(indentString) {
    this.indent = indentString;
  }

  /**
   * Sets the reference attributes to be emitted at the end of the file.
   * @param {Object.<string, string>} refs
   */
  setRefAttributes(refs) {
    this.refAttributes = refs || {};
  }

  /**
   * Exports a TreeStructInfo-compliant object to its textual representation.
   * @param {{version: string, name?: string, data: Object}} obj
   * @returns {string} The serialized TreeStructInfo string.
   * @throws {Error} If the object is not valid.
   */
  export(obj) {
    if (!obj || typeof obj !== "object" || !obj.version) {
      throw new Error("Invalid TreeStructInfo object");
    }

    let output = [];

    // Header
    let header = `treestructinfo "${obj.version}"`;
    if (obj.name) header += ` name "${obj.name}"`;
    output.push(header);

    // Tree
    output.push(...this.exportTree(obj.data, 1));

    output.push("end tree");

    // Reference attributes
    for (const [key, val] of Object.entries(this.refAttributes)) {
      output.push(`ref attr ${key} "${this.escape(val)}"`);
    }

    return output.join("\n");
  }

  /**
   * @private
   * Recursively exports nested objects as tree nodes or attributes.
   * @param {Object} data
   * @param {number} level
   * @returns {string[]} Lines of the TreeStructInfo body.
   */
  exportTree(data, level) {
    const lines = [];
    const indent = this.indent.repeat(level);

    for (const [key, value] of Object.entries(data)) {
      if (this.isPlainObject(value)) {
        lines.push(`${indent}node ${key}`);
        lines.push(...this.exportTree(value, level + 1));
        lines.push(`${indent}end node`);
      } else {
        lines.push(`${indent}attr ${key} "${this.stringify(value)}"`);
      }
    }

    return lines;
  }

  /**
   * @private
   * Converts a value to a TreeStructInfo-compatible string.
   * Handles types like boolean, number, string, Buffer, Uint8Array.
   * @param {any} value
   * @returns {string}
   */
  stringify(value) {
    if (typeof value === "boolean") {
      return value ? "true" : "false";
    } else if (typeof value === "number") {
      return value.toString();
    } else if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
      return btoa(String.fromCharCode(...new Uint8Array(value)));
    } else if (typeof value === "string") {
      return this.escape(value);
    } else if (value instanceof Buffer) {
      return value.toString("base64");
    } else {
      return this.escape(String(value));
    }
  }

  /**
   * @private
   * Escapes special characters in strings (e.g. quotes).
   * @param {string} str
   * @returns {string}
   */
  escape(str) {
    return str.replace(/"/g, '\\"');
  }

  /**
   * @private
   * Checks if the value is a plain object (not array or null).
   * @param {any} obj
   * @returns {boolean}
   */
  isPlainObject(obj) {
    return typeof obj === "object" && obj !== null && !Array.isArray(obj);
  }
}

Przykład użycia eksportera:

const exporter = new TreeStructInfoExporterJS();
exporter.setRefAttributes({ secret: "abc123" });

const output = exporter.export({
  version: "2.0",
  name: "TestDoc",
  data: {
    title: "Hello",
    flag: true,
    section: {
      count: 42
    }
  }
});
console.log(output);

Wynik:

treestructinfo "2.0" name "TestDoc"
  attr title "Hello"
  attr flag "true"
  node section
    attr count "42"
  end node
end tree
ref attr secret "abc123"
Notatka

W tym przypadku widać że brakuje w strukturze drzewa atrybutu referencjonowanego secret i trzeba go później dopisać ręcznie.

Cechy wspólne

Funkcja Parser Eksporter
Obsługa wersji
Wczytywanie nazw
Auto-casting typów
Węzły i atrybuty
Atrybuty referencyjne
Komentarze :: (ignoruje) (pomija)

Przykładowy test

const parser = new TreeStructInfoParserJS();
parser.setAutoCasting(true);

const exporter = new TreeStructInfoExporterJS();

const text = `
treestructinfo "2.0" name "Sample"
  attr name "Tree"
  attr value "123"
  node info
    attr valid "true"
  end node
end tree
`;

const parsed = parser.parse(text);
const reExported = exporter.export(parsed);

console.log(reExported);

Dokumentacja

http://www.dariuszrorat.ugu.pl/dokumenty/16-dokumentacja-treestructinfoparserjs
http://www.dariuszrorat.ugu.pl/dokumenty/17-dokumentacja-treestructinfoexporterjs

BSD-3-Clause License Agreement

BSD-3-Clause
Сopyright (c) 2025 Dariusz Rorat

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.

  2. Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.

  3. Neither the name of the copyright holder nor the names of its
    contributors may be used to endorse or promote products derived from
    this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Podsumowanie

TreeStructInfoJS to kompletna implementacja parsera i eksportera tekstowego formatu TreeStructInfo 2.0 w JavaScript. Dzięki automatycznemu rzutowaniu typów, obsłudze atrybutów referencyjnych i czytelnej strukturze kodu, biblioteka sprawdza się zarówno do edytowalnych danych konfiguracyjnych, jak i strukturalnych zapisów danych.

Możliwość pracy offline i łatwe przystosowanie do YAML/JSON/HTML sprawiają, że TreeStructInfoJS doskonale wpisuje się w potrzeby aplikacji frontendowych i backendowych.

Strona formatu TreeStructInfo:
https://tsinfo.4programmers.net/pl/index.htm

Moja aplikacja online do testowania formatu:
http://www.dariuszrorat.ugu.pl/aplikacje/treestructinfo-tester

Kody źródłowe do pobrania:
http://www.dariuszrorat.ugu.pl/uploads/media/2025/07/tsinfo.zip

15 lipca 2025 4

Kategorie

programowanie

Dziękujemy!
()

Powiązane wpisy


Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.