- エーオーシステム コーポレートサイト
https://www.aosystem.co.jp/ - エーオーシステム プロダクトサイト
https://ao-system.net/ - レンタルサーバー
- バーチャル展示会
- ウェブカタログサービス
- 3Dグラフィック
- Android アプリ
- iOS (iPhone,iPad) アプリ
- Flutter開発
- プログラミング記録QuickAnswer
- 無料画像素材
- スカイボックス 3D SKY BOX
このページのQRコード
div {
max-width: 1000px;
margin-inline: auto;
> h1 {
color: blue;
@media (width < 500px) {
color: red;
}
}
}
<style>
div { max-width: 1000px; }
div { margin-inline: auto; }
div > h1 { color: blue; }
@media (max-width: 499.98px) { div > h1 { color: red; }}
</style>
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<!-- TODO: content here -->
<script defer src="./js/css_flatten_link.js" type="module"></script>
</body>
</html>
/index.html
/css/style.css
/js/css_flatten_link.js
/js/css_flattener.class.js
<!doctype html>
<html>
<head>
<style>
/* フラット化されたCSSが配置される */
</style>
</head>
<body>
<!-- TODO: content here -->
<script defer src="./js/css_flatten_link.js" type="module"></script>
</body>
</html>
/**
* ネスト記法のCSSをフラットなCSSに変換する
* @author ao-system, Inc.
* @date 2024-11-08
*/
export class CSSFlattener {
#cssString = ''; // 元のCSS文字列
#lines = []; // CSS文字列を行ごとに分割した配列
#selectorStack = []; // 現在のセレクタのスタック(各レベルでのセレクタ配列)
#outputLines = []; // 変換後のCSS行を格納する配列
#blockStack = []; // 現在のブロック(セレクタやat-rule)のスタック
#inCommentBlock = false; // コメントブロック内か否かのフラグ
#inKeyframesBlock = false; // @keyframesブロック内か否かのフラグ
#keyframesBraceCount = 0; // @keyframesブロック内の波括弧のカウント
#includeComments = true; // コメントを出力するか否かのフラグ
#cssFilePath = ''; // CSSファイルのパス
#htmlFilePath = ''; // HTMLファイルのパス
constructor() {
}
// コメントを出力するか否かのフラグを設定する
setIncludeComments(includeComments) {
this.#includeComments = includeComments;
}
// CSSファイルのパスとHTMLファイルのパスを設定する
setFilePaths(cssFilePath, htmlFilePath) {
this.#cssFilePath = cssFilePath;
this.#htmlFilePath = htmlFilePath;
}
// セレクタを処理して、親セレクタとの組み合わせを行い、フルセレクタを生成
#processSelector(selector) {
let parentSelectors = this.#selectorStack[this.#selectorStack.length - 1] || [''];
// カンマ区切りのセレクタを分割
let selectors = selector.split(',').map(s => s.trim());
// 親セレクタとの組み合わせを行う
let combinedSelectors = [];
for (let parent of parentSelectors) {
for (let sel of selectors) {
let fullSelector = '';
if (sel.startsWith('>')) {
// 直下の子セレクタの場合
sel = sel.slice(1).trim();
fullSelector = parent ? `${parent} > ${sel}` : `> ${sel}`;
} else if (sel.includes('&')) {
// 親セレクタの参照を置換
fullSelector = parent ? sel.replace(/&/g, parent) : sel.replace(/&/g, '');
} else {
// 通常の子孫セレクタの場合
fullSelector = parent ? `${parent} ${sel}` : sel;
}
combinedSelectors.push(fullSelector.trim());
}
}
return combinedSelectors;
}
// カスタムなメディアクエリの条件を標準の形式に変換
#convertMediaQuery(condition) {
// (width < 550px) を (max-width: 549.98px) に変換
condition = condition.replace(/\(width\s*<\s*(\d+\.?\d*)px\)/g, (match, p1) => {
let value = parseFloat(p1) - 0.02;
return `(max-width: ${value}px)`;
});
// (width <= 550px) を (max-width: 550px) に変換
condition = condition.replace(/\(width\s*<=\s*(\d+\.?\d*)px\)/g, (match, p1) => {
return `(max-width: ${p1}px)`;
});
// (width > 550px) を (min-width: 550.02px) に変換
condition = condition.replace(/\(width\s*>\s*(\d+\.?\d*)px\)/g, (match, p1) => {
let value = parseFloat(p1) + 0.02;
return `(min-width: ${value}px)`;
});
// (width >= 550px) を (min-width: 550px) に変換
condition = condition.replace(/\(width\s*>=\s*(\d+\.?\d*)px\)/g, (match, p1) => {
return `(min-width: ${p1}px)`;
});
return condition;
}
// @keyframesブロック内の不要なスペースを削除して、1行の文字列に結合
#minifyKeyframes(lines) {
return lines.map(line => line.trim()).join('');
}
// CSS 内の url() を正しいパスに置換する
#adjustUrls(cssText) {
const urlPattern = /url\(\s*(['"]?)(.*?)\1\s*\)/g; //'
const cssDir = this.#cssFilePath.substring(0, this.#cssFilePath.lastIndexOf('/'));
const htmlDir = this.#htmlFilePath.substring(0, this.#htmlFilePath.lastIndexOf('/'));
return cssText.replace(urlPattern, (match, quote, url) => {
// 絶対URLの場合はそのまま
if (/^(data:|http:\/\/|https:\/\/|\/\/)/.test(url)) {
return `url(${quote}${url}${quote})`;
}
// 相対パスを解決
const resolvedUrl = this.#relativePath(cssDir, htmlDir, url);
return `url(${quote}${resolvedUrl}${quote})`;
});
}
// 相対パスを計算するヘルパーメソッド
#relativePath(fromDir, toDir, relativeUrl) {
// CSSファイル内のURLを絶対パスに変換
const absoluteUrl = this.#resolvePath(relativeUrl, fromDir + '/');
// HTMLファイルからの相対パスを計算
const relativePath = this.#getRelativePath(toDir + '/', absoluteUrl);
return relativePath;
}
// パスを解決するヘルパーメソッド
#resolvePath(relativePath, basePath) {
const stack = basePath.split('/');
const parts = relativePath.split('/');
stack.pop(); // 現在のファイル名(または空文字)を削除
for (let i = 0; i < parts.length; i++) {
if (parts[i] === '.' || parts[i] === '') continue;
if (parts[i] === '..') {
if (stack.length > 1) stack.pop();
} else {
stack.push(parts[i]);
}
}
return stack.join('/');
}
// from から to への相対パスを計算する
#getRelativePath(from, to) {
const fromParts = from.split('/').filter(Boolean);
const toParts = to.split('/').filter(Boolean);
while (fromParts.length && toParts.length && fromParts[0] === toParts[0]) {
fromParts.shift();
toParts.shift();
}
const up = fromParts.map(() => '..');
return up.concat(toParts).join('/');
}
// メインの処理関数。入力されたCSSを解析し、フラットなCSSに変換
flatten(cssString) {
this.#cssString = this.#adjustUrls(cssString); // URL を調整
this.#lines = this.#cssString.split('\n');
this.#selectorStack = [];
this.#outputLines = [];
this.#blockStack = [];
this.#inCommentBlock = false;
this.#inKeyframesBlock = false;
this.#keyframesBraceCount = 0;
let keyframesLines = [];
let selectorBuffer = '';
let collectingSelectors = false;
let i = 0;
while (i < this.#lines.length) {
let line = this.#lines[i];
let trimmedLine = line.trim();
// コメントブロック内の場合
if (this.#inCommentBlock) {
if (this.#includeComments) {
this.#outputLines.push(line);
}
if (trimmedLine.endsWith('*/')) {
this.#inCommentBlock = false;
}
i++;
continue;
}
// コメントブロックの開始
if (trimmedLine.startsWith('/*')) {
this.#inCommentBlock = true;
if (this.#includeComments) {
this.#outputLines.push(line);
}
if (trimmedLine.endsWith('*/')) {
// 同じ行でコメントが終了
this.#inCommentBlock = false;
}
i++;
continue;
}
// @keyframesブロックの開始
if (trimmedLine.startsWith('@keyframes')) {
this.#inKeyframesBlock = true;
this.#keyframesBraceCount = 0;
keyframesLines = [];
if (trimmedLine.endsWith('{')) {
this.#keyframesBraceCount++;
}
keyframesLines.push(trimmedLine);
i++;
continue;
}
// @keyframesブロック内の処理
if (this.#inKeyframesBlock) {
// 行を収集し、波括弧の数をカウント
if (trimmedLine.includes('{')) {
this.#keyframesBraceCount += (trimmedLine.match(/{/g) || []).length;
}
if (trimmedLine.includes('}')) {
this.#keyframesBraceCount -= (trimmedLine.match(/}/g) || []).length;
}
keyframesLines.push(trimmedLine);
if (this.#keyframesBraceCount === 0) {
// ブロックの終了、不要なスペースを削除して出力
let minifiedKeyframes = this.#minifyKeyframes(keyframesLines);
this.#outputLines.push(minifiedKeyframes);
this.#inKeyframesBlock = false;
}
i++;
continue;
}
// 空行はスキップ
if (trimmedLine.length === 0) {
i++;
continue;
}
// 既にフラット化されたCSSルールを検出
if (this.#isFlatCSSRule(trimmedLine)) {
// 複数行に渡る可能性があるので、ブロックの終わりまで読み込む
let flatCSSLines = [];
let braceCount = 0;
do {
flatCSSLines.push(this.#lines[i]);
if (this.#lines[i].includes('{')) {
braceCount += (this.#lines[i].match(/{/g) || []).length;
}
if (this.#lines[i].includes('}')) {
braceCount -= (this.#lines[i].match(/}/g) || []).length;
}
i++;
} while (braceCount > 0 && i < this.#lines.length);
// そのまま出力
this.#outputLines.push(flatCSSLines.join('\n'));
continue;
}
// セレクタ行の処理
if (collectingSelectors) {
// セレクタリストの続き
selectorBuffer += ' ' + trimmedLine;
if (trimmedLine.endsWith('{')) {
// セレクタリストの終了
selectorBuffer = selectorBuffer.slice(0, -1).trim(); // '{' を削除
let combinedSelectors = this.#processSelector(selectorBuffer);
this.#selectorStack.push(combinedSelectors);
this.#blockStack.push({ type: 'selector', name: combinedSelectors });
selectorBuffer = '';
collectingSelectors = false;
}
i++;
continue;
}
if (trimmedLine.endsWith(',')) {
// セレクタリストの開始(複数行にわたる)
collectingSelectors = true;
selectorBuffer = trimmedLine.slice(0, -1).trim() + ',';
i++;
continue;
}
if (trimmedLine.endsWith('{')) {
// セレクタリストが一行で完結している場合
let selector = trimmedLine.slice(0, -1).trim();
let combinedSelectors = this.#processSelector(selector);
this.#selectorStack.push(combinedSelectors);
this.#blockStack.push({ type: 'selector', name: combinedSelectors });
i++;
continue;
}
// ブロックの終了
if (trimmedLine === '}') {
let lastBlock = this.#blockStack.pop();
if (lastBlock && lastBlock.type === 'selector') {
this.#selectorStack.pop();
}
i++;
continue;
}
// プロパティ行の処理
if (this.#selectorStack.length > 0) {
let currentSelectors = this.#selectorStack[this.#selectorStack.length - 1];
let atRules = this.#blockStack.filter(block => block.type === 'atRule').map(block => block.name);
let propertyLine = trimmedLine;
if (atRules.length > 0) {
// at-rule内のプロパティ
let atRulePrefix = atRules.join(' { ') + ' { ';
let closingBraces = ' }'.repeat(atRules.length);
for (let selector of currentSelectors) {
this.#outputLines.push(`${atRulePrefix}${selector} { ${propertyLine} }${closingBraces}`);
}
} else {
// 通常のプロパティ
for (let selector of currentSelectors) {
this.#outputLines.push(`${selector} { ${propertyLine} }`);
}
}
}
i++;
}
}
// 既にフラット化されているCSSルールを検出する
#isFlatCSSRule(line) {
// 単純な正規表現でフラットなCSSルールを検出
const flatCSSRulePattern = /^[^\{\}]+\{\s*[^{}\n]+\s*\}$/;
return flatCSSRulePattern.test(line);
}
// 変換後のCSSを取得
getOutput() {
return this.#outputLines.join('\n');
}
}
/**
* CSSファイルを読み込み、CSSをフラット化して<head>内に<style>タグとして挿入する
* ローカルパス指定のlinkが対象
*
* @author ao-system, Inc.
* @date 2024-11-08
*/
import { CSSFlattener } from './css_flattener.class.js';
(() => {
new class {
constructor() {
if (this.#isAtLeastChrome125()) {
return; //iOS17以上の場合は処理しない
}
document.addEventListener('DOMContentLoaded', () => { this.#flattenCSSLinks(); });
}
#flattenCSSLinks() {
// すべての <link rel="stylesheet"> 要素を選択
const allLinks = document.querySelectorAll('link[rel="stylesheet"]');
// 相対パスの href を持つリンクのみをフィルタリング
const links = Array.from(allLinks).filter(link => {
const href = link.getAttribute('href');
return href && this.#isRelativePath(href);
});
links.forEach(link => {
const href = link.getAttribute('href');
if (href) {
// CSSファイルを非同期で取得
fetch(href)
.then(response => {
if (!response.ok) {
throw new Error(`Failed to load CSS file: ${href}`);
}
return response.text();
})
.then(nestedCssText => {
// CSSFlattenerを初期化
const flattener = new CSSFlattener();
flattener.setIncludeComments(false); // コメントを出力しない
// CSSファイルのパスとHTMLファイルのパスを設定
const cssFilePath = this.#getAbsolutePath(href);
const htmlFilePath = window.location.pathname;
flattener.setFilePaths(cssFilePath, htmlFilePath);
// CSSをフラット化
flattener.flatten(nestedCssText);
const flattenedCSS = flattener.getOutput();
// <style>タグを作成し、フラット化したCSSを挿入
const styleElement = document.createElement('style');
styleElement.textContent = flattenedCSS;
document.head.appendChild(styleElement); // <head>内に<style>タグを追加
// 元の<link>タグを削除
link.parentNode.removeChild(link);
// デバッグ用に出力
// console.log(flattenedCSS);
})
.catch(error => {
console.error(error);
});
}
});
}
//Chrome125で導入された重複した名前付きキャプチャグループをテスト chrome125以上ならtrue iOS17以上ならtrue
#isAtLeastChrome125() {
try {
new RegExp('(?<year>[0-9]{4})-[0-9]{2}|[0-9]{2}-(?<year>[0-9]{4})');
return true;
} catch (e) {
return false;
}
}
//相対パスかどうかを判定する
#isRelativePath(href) {
// 絶対URL(http://、https://、//)で始まる場合は除外
return !/^(?:[a-z]+:)?\/\//i.test(href);
}
// 絶対パスを取得する
#getAbsolutePath(href) {
const link = document.createElement('a');
link.href = href;
return link.pathname;
}
}
})();
このページのQRコード
便利ウェブサイト
便利 Android アプリ
便利 iOS(iPhone,iPad) アプリ