Що таке html препроцесор
HTML препроцесор — це скрипт, який перетворює певний формат мови розмітки в звичайний HTML. Навіщо ж він потрібен? Чим не влаштовує звичайний html? Основна причина — це зручно, це прискорює процес верстки сайту.
У більшості випадків, на кожній сторінці сайту присутні однакові блоки (хедер, футер, сайтбар та ін). Що б замінити напис, наприклад, в хедері, потрібно вручну вносити зміни на кожній сторінці сайту. HTML шаблонизатор дозволяє винести все повторюється елементи в один базовий шаблон. При цьому змінити напис в хедері на всьому сайті займе пару хвилин.
Також, уявіть, що у вас на сторінці список з понад 10 карток товару, і потрібно в кожній внести зміни. HTML препроцесор дозволяє зберігати інформацію в масиві об’єктів і виводити список елементів в циклі. Це набагато зручніше ніж міняти в розмітці HTML.
Основні переваги використання HTML препроцесорів:
- Використання базових шаблонів (layout)
- Вставка шаблонів в інші шаблони (include)
- Змінні
- Цикли
- Міксини
- І багато іншого
Список html препроцесорів:
Ось список з найбільш відомих HTML препроцесорів:
- Nunjucks
- Pug
- Slim
- HAML
- Macdom
Шаблонизатор nunjucks
По суті, Nunjucks — це потужний шаблонизатор для JavaScript. Його можна використовувати як в браузері, на стороні клієнта, так і на стороні сервера Node.js. Але також, Nunjucks дуже зручно застосовувати для верстки сайтів.
Чому саме Nunjucks?
- Це повноцінна мова, в якому є все, що може знадобитися для frontend розробки
- На відміну від інших html препроцесорів, синтаксис в nunjucks не відрізняється від звичайного HTML
Як користуватися шаблонізатором html nunjucks (gulp nunjucks)
Для цього найпростіше використовувати таск-менеджер gulp і модуль gulp-nunjucks. Встановлювати їх будемо за допомогою утиліти npm (для цього попередньо потрібно встановити Node.js собі на комп’ютер).
Заходимо з консолі в папку з проектом та ініціалізуемо npm:
npm i
В результаті отримаємо файл package.json, в якому описуються всі залежності та налаштування.
Далі встановлюємо gulp і gulp-nunjucks:
npm install -D gulp gulp-nunjucks
Створюємо файл gulpfile.js. У ньому знаходяться набори команд для gulp. Приклад простої конфігурації для nunjucks:
const gulp = require('gulp');
const nunjucks = require('gulp-nunjucks');
function njk(){
return gulp.src('src/*.html')
.pipe(nunjucks.compile())
.pipe(gulp.dest('dist'))
};
function watch(){
gulp.watch('src/**/*.html', njk);
}
exports.default = gulp.series(njk, watch);
Щоб б не встановлювати gulp глобально, відкриваємо файл package.json, в розділ scripts додаємо рядок:
"gulp": "gulp"
В результаті отримуємо такий файл package.json:
{
"name": "nunjucks",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"gulp": "gulp",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"gulp": "^4.0.2",
"gulp-nunjucks": "^5.1.0"
}
}
Це простий приклад налаштування gulp для демонстрації роботи nunjucks. Більш детально, як працювати з gulp, ми розглянемо в наступних статтях.
Далі створимо в корені проекту папку src і dist. У src знаходяться исходники nunjucks. Gulp буде відслідковувати зміну в цьому каталозі і зберігати готові html файли в папку dist.
Щоб запустити gulp, в командному рядку пишемо:
npm run gulp
Приклад роботи з nunjucks
І так, ми підготували оточення, налаштували gulp, і можемо пробувати nunjucks в справі. Для прикладу, візьмемо простий bootstrap шаблон, що складається з 3-х сторінок, написаний на звичайному html, і переробимо його на шаблонизатор nunjucks.
Спадкування шаблонів
Nunjucks дає можливість успадковувати шаблони. Іншими словами, ви можете створювати сторінки сайту по заздалегідь створеним шаблонами. Спадкування працює через конструкції block і extends.
Подивимося на наш приклад:
Всі сторінки мають загальний каркас (верхня частина, футер, сайтбар) і змінну частину. На сторінці контакти присутній додатковий блок в сайтбаре. Також на всіх сторінках, окрім головної, є навігаційний ланцюг (до цього ми повернемося пізніше).
Створюємо в каталозі src папку layouts. У ній створюємо файл нашого основного шаблону default.html. Копіюємо туди все спільне, що є на всіх сторінках, а замість змінної частини пропишемо конструкцію block:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home title</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-inverse">
<div class="container">
<a class="navbar-brand" href="">(123) 456-7890</a>
<a class="navbar-brand" href="mail:name@example.com">name@example.com</a>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li class="active"><a href="index.html">Home</a></li>
<li><a href="services.html">Service</a></li>
<li><a href="contact.html">Contact</a></li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">Home</h1>
<ol class="breadcrumb">
<li><a href="index.html">Home</a></li>
<li class="active">Service</li>
</ol>
</div>
</div>
<div class="row">
<div class="col-md-3">
{% block aside %}
<div class="list-group">
<a href="index.html" class="list-group-item active">Home</a>
<a href="services.html" class="list-group-item">Service</a>
<a href="contact.html" class="list-group-item">Contact</a>
</div>
{% endblock %}З основного шаблони
</div>
<div class="col-md-9">{% block content %}{% endblock %}</div>
</div>
<footer style="padding: 50px 0; margin-top:20px; border-top: 1px solid #e3e3e3;">
<div class="row">
<div class="col-lg-12">
<p>Copyright © 2021</p>
</div>
</div>
</footer>
</div>
</body>
</html>
І так, ми додали {% block content%} {% endblock%} в те місце, де підставляється основна інформація сторінки. Також додали {% block aside%} {% endblock%} для виведення сайтбара, але між цими тегами вставили блок за замовчуванням, так як на більшості сторінках він однаковий.
В папці src створюємо файл index.html, в ньому успадковуємо наш базовий шаблон і в блоці content ми можемо підставити вміст сторінки сайту:
{% extends "layouts/default.html" %}
{% block content %}
<h1>Hello world!</h1>
{% endblock %}
В результаті отримаємо:
Тепер ми можемо змінити меню або футер в одному місці, і зміни застосуються на всьому сайті.
Створюємо всі відсутні сторінки і підставимо актуальну інформацію на всіх сторінках.
Повернемося до сторінці контакти. Крім контентної частини, в ній необхідно перевизначити сайтбар:
{% extends "layouts/default.html" %}
{% block content %}
<!—- Контент сторінки -->
{% endblock %}
{% block aside %}
<div class="list-group">
<a href="index.html" class="list-group-item active">Home</a>
<a href="services.html" class="list-group-item">Service</a>
<a href="contact.html" class="list-group-item">Contact</a>
</div>
<h3>Contact Details</h3>
<p><abbr title="Phone">Phone</abbr>:<a href="">(123) 456-7890</a></p>
<p><abbr title="Email">Email</abbr>:<a href="mailto:name@example.com">name@example.com</a></p>
{% endblock %}
Включення одного шаблону в інший (nunjucks include)
Nunjucks дозволяє вставляти вміст одного файлу в інший. Це дає можливість виносити в окремі файли розмітку повторюваних блоків. Такий підхід зробить наш код більш зручним і простим у підтримці. Вставка одного файлу в інший здійснюється конструкцією include:
{% include “filename” %}
Давайте на нашому сайті header, footer і sitebar винесемо в окремі файли. В папці src створимо каталог parts, в якій будемо зберігати всі різні частини коду.
Створюємо файли header.html, footer.html і sidebar.html. З основного шаблони вирізаємо необхідні частини, а замість них прописуємо конструкцію include. В результаті файл default.html буде виглядати так:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home title</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
{% include 'parts/header.html' %}
<div class="container">
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">Home</h1>
<ol class="breadcrumb">
<li><a href="index.html">Home</a></li>
<li class="active">Service</li>
</ol>
</div>
</div>
<div class="row">
<div class="col-md-3">
{% block aside %}
{% include 'parts/sidebar.html' %}
{% endblock %}
</div>
<div class="col-md-9">{% block content %}{% endblock %}</div>
</div>
{% include 'parts/footer.html' %}
</div>
</body>
</html>
Так само, на сторінці контакти нам необхідно підключити файл sidebar.html, так як ми перевизначаємо на ній сайтбар:
{% block aside %}
{% include 'parts/sidebar.html' %}
<h3>Contact Details</h3>
<p><abbr title="Phone">Phone</abbr>:<a href="">{{ data.phone }}</a></p>
<p><abbr title="Email">Email</abbr>:<a href="mailto:{{ data.email }}">{{ data.email }}</a></p>
{% endblock %}
Змінні (nunjucks Variables)
Дуже часто виникає ситуація, коли одні і ті ж дані виводяться в різних місцях сайту. Наприклад, контактні дані, заголовки сторінок і ін. Для цього використовують змінні. Вони дозволяють задавати необхідні значення на початку коду і виводити їх в потрібних ділянках коду.
У nunjucks змінні задаються наступною конструкцією:
{% set username = "Ivan" %}
Висновок вмісту змінної:
{{ Ivan }}
Подивимося на шаблон default.html. У ньому title і заголовок сторінок жорстко прописані в коді. Це нам не підходить, так як ці значення різні на всіх сторінках. Тому, на кожній сторінці заведемо змінні title і pageName і додамо їх на початку сторінок:
{% set title = "Home title" %}
{% set pageName = "Home" %}
{% extends "layouts/default.html" %}
...
У шаблоні виведемо ці значення:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
{% include 'parts/header.html' %}
<div class="container">
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">{{ pageName }}</h1>
...
У підсумку, у кожної сторінки свій title і заголовок.
Nunjucks import
Конструкція import дозволяє завантажувати шаблони і отримувати доступ до його експортованих значенням (змінним і міксин).
Давайте ще подивимося на наш сайт. У верхній частині і в сайтбаре виводиться контактна інформація:
Її також зручно зберігати в змінні. Але ці змінні стосуються всього сайту, а не окремих сторінок. Тому, створимо файл, де будемо зберігати глобальні змінні для всього сайту і імпортуємо його в основному шаблоні. Створюємо папку data в src, в ній файл data.html. На самому початку файлу default.html прописуємо конструкцію import:
{% import "data/data.html" as data %}
Теперь можно получить доступ ко всем значениям в data.html через переменную data.
В data.html добавим 2 переменные:
{% set phone = "(123) 456-7890" %}
{% set email = "name@example.com" %}
Тепер в файлах header.html і contact.html виводимо ці змінні:
<nav class="navbar navbar-inverse">
<div class="container">
<a class="navbar-brand" href="">{{ data.phone }}</a>
<a class="navbar-brand" href="mail:{{ data.email }}">{{ data.email }}</a>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li class="active">
<a href="index.html">Home</a>
</li>
<li>
<a href="services.html">Service</a>
</li>
<li>
<a href="contact.html">Contact</a>
</li>
</ul>
</div>
</div>
</nav>
...
{% block aside %}
<div class="list-group">
<a href="index.html" class="list-group-item active">Home</a>
<a href="services.html" class="list-group-item">Service</a>
<a href="contact.html" class="list-group-item">Contact</a>
</div>
<h3>Contact Details</h3>
<p><abbr title="Phone">Phone</abbr>:<a href="">{{ data.phone }}</a></p>
<p><abbr title="Email">Email</abbr>:<a href="mailto:{{ data.email }}">{{ data.email }}</a></p>
{% endblock %}
Умови (Nunjucks if)
У nunjucks, як і в інших шаблонизатор, є умовний оператор if. If перевіряє умови і дозволяє вибірково відображати вміст:
{% if variable %}
Показуємо контент
{% endif %}
Також є можливість вказати альтернативні умови за допомогою конструкцій elseif і else:
{% if page == ‘home’ %}
Контент для главной страницы
{% elseif page==’contcat’ %}
Контент для страницы контакты
{% else %}
Контент для остальных страниц
{% endif %}
Повернемося до тестового сайту. Як ми пам’ятаємо, навігаційній ланцюг не повинен виводиться на головній сторінці. Давайте це виправимо.
На кожній сторінці додамо ще по одній змінної:
Головна сторінка:
{% set pageSlug = "home" %}
Сторінка послуг:
{% set pageSlug = "service" %}
Сторінка контактів:
{% set pageSlug = "contact" %}
Цими змінними ми задаємо унікальну назву (ярлик) для кожної сторінки.
Далі, в основному шаблоні задаємо умову для виведення навігаційного ланцюга:
...
{% if pageSlug != 'home' %}
<ol class="breadcrumb">
<li><a href="index.html">Home</a></li>
<li class="active">Service</li>
</ol>
{% endif %}
...
Якщо змінна pageSlug не дорівнює home, то ми виводимо блок з контентом. Таким чином, ми виводимо хлібні крихти на всіх сторінках, окрім головної.
Тернарний оператор (If Expression)
Тернарний оператор – конструкція, яка повертає друге або третє значення, в залежності від логічного виразу. Щось подібне є і в nunjucks:
{{ "home" if page == ’home’ else "default" }}
Якщо умова істинно, то виведеться home, в іншому випадку default.
На нашому тестовому сайті в навігації не визначається активна сторінка. Заходимо в файл header.html, знаходимо місце де виводиться меню і застосуємо тернарний оператор:
...
<ul class="nav navbar-nav navbar-right">
<li class="{{ 'active' if pageSlug == 'home' }}"><a href="index.html">Home</a></li>
<li class="{{ 'active' if pageSlug == 'service' }}"><a href="services.html">Service</a></li>
<li class="{{ 'active' if pageSlug == 'contact' }}"><a href="contact.html">Contact</a></li>
</ul>
...
Теж саме робимо в файлі sidebar.html :
<div class="list-group">
<a href="index.html" class="list-group-item {{ 'active' if pageSlug == 'home' }}">Home</a>
<a href="services.html" class="list-group-item {{ 'active' if pageSlug == 'service' }}">Service</a>
<a href="contact.html" class="list-group-item {{ 'active' if pageSlug == 'contact' }}">Contact</a>
</div>
Цикли
Цикли — це операція, при якій одна й та ж дія виконується кілька разів. У nunjucks так само є конструкція для організації циклів:
{% set users = [
{
'name': 'Борис',
'surname': 'Иващенко'
},
{
'name': 'Эрик',
'surname': 'Николаев'
}
] %}
{% for(item in users) %}
<p>Name: {{ item.name}}. Surname: {{item.surname }} </p>
{% endfor %}
У наведеному прикладі перераховані всі користувачі, використовуючи атрибути name і surname кожного елемента в масиві users.
Всередині циклу є доступ до декількох змінним:
- Loop.index — поточна ітерація циклу, починаючи з 1
- Loop.index0 — поточна ітерація циклу, починаючи з 0
- Loop.revindex — кількість ітерацій до кінця, починаючи з 1
- Loop.revindex0 — кількість ітерацій до кінця, починаючи з 0
- Loop.first — логічне значення, яке вказує першу ітерацію
- Loop.last — логічне значення, яке вказує останню ітерацію
- Loop.length — загальна кількість елементів
Подивимося як використовувати цикли на прикладі. Повернемося до навігаційного ланцюга, зараз він заданний жорстко в шаблоні і виводитися некоректно. Давай це виправимо.
На сторінках, де необхідно виводити хлібні крихти, задамо масив з необхідними елементами:
Сторінка послуг:
{% set breadcrumbs = [
{
'name': 'Home',
'url': 'index.html'
},
{
'name': 'Service',
'url': 'service.html'
}
] %}
Сторінка контакти:
{% set breadcrumbs = [
{
'name': 'Home',
'url': 'index.html'
},
{
'name': 'Contact',
'url': 'contact.html'
}
] %}
В основному шаблоні виведемо елементи навігаційної ланцюжка:
...
{% if pageSlug != 'home' %}
<ol class="breadcrumb">
{% for item in breadcrumbs %}
{% if loop.last %}
<li class="active">{{ item.name }}</li>
{% else %}
<li><a href="{{ item.url }}">{{ item.name }}</a></li>
{% endif %}
{% endfor %}
</ol>
{% endif %}
...
У циклі ми перевіряємо, якщо елемент масиву останній, то виводимо його як активний.
Так само, давайте виведемо наше меню в циклі. Для цього в файлі data.html створимо масив з елементами навігації:
{% set menu = [
{
'title': 'Home',
'url': 'index.html',
'slug': 'home'
},
{
'title': 'Service',
'url': 'services.html',
'slug': 'service'
},
{
'title': 'Contact',
'url': 'contact.html',
'slug': 'contact'
}
] %}
Виведемо меню в файлі header.html:
...
<ul class="nav navbar-nav navbar-right">
{% for item in data.menu %}
<li class="{{ 'active' if pageSlug == item.slug }}"><a href="{{item.url}}">{{item.title}}</a></li>
{% endfor %}
</ul>
...
Аналогічно у файлі sidebar.html:
<div class="list-group">
{% for item in data.menu %}
<a href="{{item.url}}" class="list-group-item {{ 'active' if pageSlug == item.slug }}">{{item.title}}</a>
{% endfor %}
</div>
Тепер елементи меню легко змінювати, додавати нові пункти навігації і зміни застосуються на всьому сайті, на всіх сторінках.
Макроси (macro)
Макроси дозволяють визначити фрагменти коду, які можна використовувати багаторазово в будь-якому шаблоні. Це схоже на функції в мовах програмування. Приклад синтаксису макросу:
{% macro User(name, isAdmin=false) %}
Користувач {{name}} — {{‘адміністратор’ if isAdmin else ‘не адміністратор’}}
{% endmacro %}
Тепер макрос з прикладу можна викликати необмежену кількість разів:
{{ User(‘Иван’) }}
{{ User(‘Николай’, true) }}
У першому варіанті ми передаємо один параметр, а значення isAdmin задана за замовчуванням, у другому варіанті ми передаємо два аргументи.
Розглянемо роботу з макросами на прикладі. На головній сторінці та сторінці сервіси є однаковий блок зі списком елементів, але різним вмістом:
Виведемо цей блок за допомогою макросу. Створюємо в папці src каталог macro. У ньому файл macro.html, в якому будемо зберігати всі наші макроси. Як і у випадку з файлом змінних, імпортуємо його в основний шаблон:
{% import "macro/macro.html" as macro %}
Далі створюємо наш макрос, назвемо його ItemList, в який передається масив з необхідними даними:
{% macro ItemList(data) %}
{% for item in data %}
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h4><i class="fa fa-fw fa-check"></i>{{ item.title }}</h4>
</div>
<div class="panel-body">
<p>{{ item.text }}</p>
<a href="{{ item.url }}" class="btn btn-default">Learn More</a>
</div>
</div>
</div>
{% endfor %}
{% endmacro %}
Після цього викликаємо макрос в потрібних місцях, передаючи в нього необхідні значення. Файл index.html:
{% set title = "Home title" %}
{% set pageName = "Home" %}
{% set pageSlug = "home" %}
{% extends "layouts/default.html" %}
{% block content %}
<div class="row">
<div class="col-lg-12">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer sit amet egestas sapien, nec ornare diam. Maecenas non arcu libero. Mauris eu leo efficitur, vehicula elit vitae, vulputate mauris. Vestibulum libero mi, iaculis et lorem id, tempus vehicula metus. Nullam ultrices nibh sem, id fermentum mi ullamcorper sit amet. Duis luctus sagittis metus, quis auctor libero semper non.</p>
<p>Etiam congue pharetra lorem in vulputate. Pellentesque pulvinar ligula vel fringilla consequat. Donec nec nisi elit. Vivamus varius est vel sodales consequat. Praesent et scelerisque eros.</p>
</div>
{{ macro.ItemList([
{
'title': 'Advantages #1',
'text': 'Advantages #1 text',
'url': 'Advantages_#1_url'
},
{
'title': 'Advantages #2',
'text': 'Advantages #2 text',
'url': 'Advantages_#2_url'
},
{
'title': 'Advantages #3',
'text': 'Advantages #3 text',
'url': 'Advantages_#3_url'
}
]) }}
</div>
{% endblock %}
Файл service.html:
{% set title = "Service title" %}
{% set pageName = "Service" %}
{% set pageSlug = "service" %}
{% set breadcrumbs = [
{
'name': 'Home',
'url': 'index.html'
},
{
'name': 'Service',
'url': 'service.html'
}
] %}
{% extends "layouts/default.html" %}
{% block content %}
<div class="row">
<div class="col-lg-12">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Soluta, et temporibus, facere perferendis veniam beatae non debitis, numquam blanditiis necessitatibus vel mollitia dolorum laudantium, voluptate dolores iure maxime ducimus fugit.</p>
<p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quasi, provident! Sed laboriosam deleniti eius. Consequatur corporis enim quisquam, maxime unde impedit dolorem voluptatem nulla saepe sint vero et omnis. Voluptatum necessitatibus autem cum officiis quam porro error molestias cumque laudantium.</p>
</div>
<div class="col-lg-12">
<h2 class="page-header">Service List</h2>
</div>
{{ macro.ItemList([
{
'title': 'Service #1',
'text': 'Service #1 text',
'url': 'Service_#1_url'
},
{
'title': 'Service #2',
'text': 'Service #2 text',
'url': 'Service_#2_url'
},
{
'title': 'Service #3',
'text': 'Service #3 text',
'url': 'Service_#3_url'
}
]) }}
</div>
{% endblock %}
Тепер у нас є макрос, який можна викликати необмежену кількість разів, передаючи потрібні дані.
Фільтри (Nunjucks filters)
Фільтри – це конструкція, за допомогою якої можна виконувати різного роду маніпуляції над переданими даними. У nunjucks є велика кількість вбудованих фільтрів, їх можна подивитися на офіційній документації.
На приклад, фільтр abs повертає абсолютне значення аргументу:
{{ -3|abs }}
Поверне 3
Фільтри можна вибудовувати в ланцюжки для послідовної обробки даних:
{{ data | filter_1 | filter_2 … | filter_n }}
Приклад використання фільтра подивимося на нашому сайті. Раніше ми створили змінну phone і привласнили їй значення (123) 456-7890. Далі необхідно вивести телефон посиланням в такому форматі:
<a href=”tel:1234567890”>(123) 456-7890</a>
Тобто, вирізати зі змінною все зайві символи і залишити тільки числа. Для цього застосуємо фільтр replace:
<a href="tel:{{ data.phone | replace('(', '') | replace(')', '') | replace(' ', '') | replace('-', '') }}">{{ data.phone }}</a>
Виводимо цю конструкцію в файлах header.html :
<a class="navbar-brand" href="tel:{{ data.phone | replace('(', '') | replace(')', '') | replace(' ', '') | replace('-', '') }}">{{ data.phone }}</a>
і conatc.html:
<p><abbr title="Phone">Phone</abbr>:<a href="tel:{{ data.phone | replace('(', '') | replace(')', '') | replace(' ', '') | replace('-', '') }}">{{ data.phone }}</a></p>
Висновки
Ми розглянули основні моменти використання шаблонізатора nunjucks. Для приклади створили невеликий сайт на цьому препроцесорів. Готовий результат можна подивитися у репозиторії. Для більш детального вивчення можна скористатися офіційною документацією по nunjucks.