зеркало из https://github.com/mozilla/pjs.git
Bug 706198 - Disable font inflation on sites optimized for mobile to avoid strangely inflated text. [r=mbrubeck,dbaron]
--HG-- extra : rebase_source : 60d764b0a76ae87515a3e669ba76adc61926c862
This commit is contained in:
Родитель
163e7715aa
Коммит
7faffa3598
|
@ -178,6 +178,48 @@ enum EventNameType {
|
||||||
EventNameType_All = 0xFFFF
|
EventNameType_All = 0xFFFF
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information retrieved from the <meta name="viewport"> tag. See
|
||||||
|
* GetViewportInfo for more information on this functionality.
|
||||||
|
*/
|
||||||
|
struct ViewportInfo
|
||||||
|
{
|
||||||
|
// Default zoom indicates the level at which the display is 'zoomed in'
|
||||||
|
// initially for the user, upon loading of the page.
|
||||||
|
double defaultZoom;
|
||||||
|
|
||||||
|
// The minimum zoom level permitted by the page.
|
||||||
|
double minZoom;
|
||||||
|
|
||||||
|
// The maximum zoom level permitted by the page.
|
||||||
|
double maxZoom;
|
||||||
|
|
||||||
|
// The width of the viewport, specified by the <meta name="viewport"> tag,
|
||||||
|
// in CSS pixels.
|
||||||
|
PRUint32 width;
|
||||||
|
|
||||||
|
// The height of the viewport, specified by the <meta name="viewport"> tag,
|
||||||
|
// in CSS pixels.
|
||||||
|
PRUint32 height;
|
||||||
|
|
||||||
|
// Whether or not we should automatically size the viewport to the device's
|
||||||
|
// width. This is true if the document has been optimized for mobile, and
|
||||||
|
// the width property of a specified <meta name="viewport"> tag is either
|
||||||
|
// not specified, or is set to the special value 'device-width'.
|
||||||
|
bool autoSize;
|
||||||
|
|
||||||
|
// Whether or not the user can zoom in and out on the page. Default is true.
|
||||||
|
bool allowZoom;
|
||||||
|
|
||||||
|
// This is a holdover from e10s fennec, and might be removed in the future.
|
||||||
|
// It's a hack to work around bugs that didn't allow zooming of documents
|
||||||
|
// from within the parent process. It is still used in native Fennec for XUL
|
||||||
|
// documents, but it should probably be removed.
|
||||||
|
// Currently, from, within GetViewportInfo(), This is only set to false
|
||||||
|
// if the document is a XUL document.
|
||||||
|
bool autoScale;
|
||||||
|
};
|
||||||
|
|
||||||
struct EventNameMapping
|
struct EventNameMapping
|
||||||
{
|
{
|
||||||
nsIAtom* mAtom;
|
nsIAtom* mAtom;
|
||||||
|
@ -1489,6 +1531,18 @@ public:
|
||||||
return sScriptBlockerCount == 0;
|
return sScriptBlockerCount == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve information about the viewport as a data structure.
|
||||||
|
* This will return information in the viewport META data section
|
||||||
|
* of the document. This can be used in lieu of ProcessViewportInfo(),
|
||||||
|
* which places the viewport information in the document header instead
|
||||||
|
* of returning it directly.
|
||||||
|
*
|
||||||
|
* NOTE: If the site is optimized for mobile (via the doctype), this
|
||||||
|
* will return viewport information that specifies default information.
|
||||||
|
*/
|
||||||
|
static ViewportInfo GetViewportInfo(nsIDocument* aDocument);
|
||||||
|
|
||||||
/* Process viewport META data. This gives us information for the scale
|
/* Process viewport META data. This gives us information for the scale
|
||||||
* and zoom of a page on mobile devices. We stick the information in
|
* and zoom of a page on mobile devices. We stick the information in
|
||||||
* the document header and use it later on after rendering.
|
* the document header and use it later on after rendering.
|
||||||
|
|
|
@ -211,6 +211,8 @@ static NS_DEFINE_CID(kXTFServiceCID, NS_XTFSERVICE_CID);
|
||||||
#include "mozilla/Preferences.h"
|
#include "mozilla/Preferences.h"
|
||||||
|
|
||||||
#include "nsWrapperCacheInlines.h"
|
#include "nsWrapperCacheInlines.h"
|
||||||
|
#include "nsIDOMDocumentType.h"
|
||||||
|
#include "nsIDOMWindowUtils.h"
|
||||||
#include "nsCharSeparatedTokenizer.h"
|
#include "nsCharSeparatedTokenizer.h"
|
||||||
#include "nsUnicharUtils.h"
|
#include "nsUnicharUtils.h"
|
||||||
|
|
||||||
|
@ -221,6 +223,17 @@ using namespace mozilla;
|
||||||
|
|
||||||
const char kLoadAsData[] = "loadAsData";
|
const char kLoadAsData[] = "loadAsData";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default values for the ViewportInfo structure.
|
||||||
|
*/
|
||||||
|
static const float kViewportMinScale = 0.0;
|
||||||
|
static const float kViewportMaxScale = 10.0;
|
||||||
|
static const PRUint32 kViewportMinWidth = 200;
|
||||||
|
static const PRUint32 kViewportMaxWidth = 10000;
|
||||||
|
static const PRUint32 kViewportMinHeight = 223;
|
||||||
|
static const PRUint32 kViewportMaxHeight = 10000;
|
||||||
|
static const PRInt32 kViewportDefaultScreenWidth = 980;
|
||||||
|
|
||||||
static const char kJSStackContractID[] = "@mozilla.org/js/xpc/ContextStack;1";
|
static const char kJSStackContractID[] = "@mozilla.org/js/xpc/ContextStack;1";
|
||||||
static NS_DEFINE_CID(kParserServiceCID, NS_PARSERSERVICE_CID);
|
static NS_DEFINE_CID(kParserServiceCID, NS_PARSERSERVICE_CID);
|
||||||
static NS_DEFINE_CID(kCParserCID, NS_PARSER_CID);
|
static NS_DEFINE_CID(kCParserCID, NS_PARSER_CID);
|
||||||
|
@ -4554,6 +4567,198 @@ static void ProcessViewportToken(nsIDocument *aDocument,
|
||||||
#define IS_SEPARATOR(c) ((c == '=') || (c == ',') || (c == ';') || \
|
#define IS_SEPARATOR(c) ((c == '=') || (c == ',') || (c == ';') || \
|
||||||
(c == '\t') || (c == '\n') || (c == '\r'))
|
(c == '\t') || (c == '\n') || (c == '\r'))
|
||||||
|
|
||||||
|
/* static */
|
||||||
|
ViewportInfo
|
||||||
|
nsContentUtils::GetViewportInfo(nsIDocument *aDocument)
|
||||||
|
{
|
||||||
|
ViewportInfo ret;
|
||||||
|
ret.defaultZoom = 1.0;
|
||||||
|
ret.autoSize = true;
|
||||||
|
ret.allowZoom = true;
|
||||||
|
ret.autoScale = true;
|
||||||
|
|
||||||
|
// If the docType specifies that we are on a site optimized for mobile,
|
||||||
|
// then we want to return specially crafted defaults for the viewport info.
|
||||||
|
nsCOMPtr<nsIDOMDocument>
|
||||||
|
domDoc(do_QueryInterface(aDocument));
|
||||||
|
|
||||||
|
nsCOMPtr<nsIDOMDocumentType> docType;
|
||||||
|
nsresult rv = domDoc->GetDoctype(getter_AddRefs(docType));
|
||||||
|
if (NS_SUCCEEDED(rv) && docType) {
|
||||||
|
nsAutoString docId;
|
||||||
|
rv = docType->GetPublicId(docId);
|
||||||
|
if (NS_SUCCEEDED(rv)) {
|
||||||
|
if ((docId.Find("WAP") != -1) ||
|
||||||
|
(docId.Find("Mobile") != -1) ||
|
||||||
|
(docId.Find("WML") != -1))
|
||||||
|
{
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aDocument->IsXUL()) {
|
||||||
|
ret.autoScale = false;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
nsIDOMWindow* window = aDocument->GetWindow();
|
||||||
|
nsCOMPtr<nsIDOMWindowUtils> windowUtils(do_GetInterface(window));
|
||||||
|
|
||||||
|
if (!windowUtils) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
nsAutoString handheldFriendly;
|
||||||
|
aDocument->GetHeaderData(nsGkAtoms::handheldFriendly, handheldFriendly);
|
||||||
|
|
||||||
|
if (handheldFriendly.EqualsLiteral("true")) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
PRInt32 errorCode;
|
||||||
|
|
||||||
|
nsAutoString minScaleStr;
|
||||||
|
aDocument->GetHeaderData(nsGkAtoms::minimum_scale, minScaleStr);
|
||||||
|
|
||||||
|
float scaleMinFloat = minScaleStr.ToFloat(&errorCode);
|
||||||
|
|
||||||
|
if (errorCode) {
|
||||||
|
scaleMinFloat = kViewportMinScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleMinFloat = NS_MIN(scaleMinFloat, kViewportMaxScale);
|
||||||
|
scaleMinFloat = NS_MAX(scaleMinFloat, kViewportMinScale);
|
||||||
|
|
||||||
|
nsAutoString maxScaleStr;
|
||||||
|
aDocument->GetHeaderData(nsGkAtoms::maximum_scale, maxScaleStr);
|
||||||
|
|
||||||
|
// We define a special error code variable for the scale and max scale,
|
||||||
|
// because they are used later (see the width calculations).
|
||||||
|
PRInt32 scaleMaxErrorCode;
|
||||||
|
float scaleMaxFloat = maxScaleStr.ToFloat(&scaleMaxErrorCode);
|
||||||
|
|
||||||
|
if (scaleMaxErrorCode) {
|
||||||
|
scaleMaxFloat = kViewportMaxScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleMaxFloat = NS_MIN(scaleMaxFloat, kViewportMaxScale);
|
||||||
|
scaleMaxFloat = NS_MAX(scaleMaxFloat, kViewportMinScale);
|
||||||
|
|
||||||
|
nsAutoString scaleStr;
|
||||||
|
aDocument->GetHeaderData(nsGkAtoms::viewport_initial_scale, scaleStr);
|
||||||
|
|
||||||
|
PRInt32 scaleErrorCode;
|
||||||
|
float scaleFloat = scaleStr.ToFloat(&scaleErrorCode);
|
||||||
|
scaleFloat = NS_MIN(scaleFloat, scaleMaxFloat);
|
||||||
|
scaleFloat = NS_MAX(scaleFloat, scaleMinFloat);
|
||||||
|
|
||||||
|
nsAutoString widthStr, heightStr;
|
||||||
|
|
||||||
|
aDocument->GetHeaderData(nsGkAtoms::viewport_height, heightStr);
|
||||||
|
aDocument->GetHeaderData(nsGkAtoms::viewport_width, widthStr);
|
||||||
|
|
||||||
|
bool autoSize = false;
|
||||||
|
|
||||||
|
if (widthStr.EqualsLiteral("device-width")) {
|
||||||
|
autoSize = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widthStr.IsEmpty() &&
|
||||||
|
(heightStr.EqualsLiteral("device-height") ||
|
||||||
|
scaleFloat == 1.0))
|
||||||
|
{
|
||||||
|
autoSize = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXXjwir3:
|
||||||
|
// See bug 706918, comment 23 for more information on this particular section
|
||||||
|
// of the code. We're using "screen size" in place of the size of the content
|
||||||
|
// area, because on mobile, these are close or equal. This will work for our
|
||||||
|
// purposes (bug 706198), but it will need to be changed in the future to be
|
||||||
|
// more correct when we bring the rest of the viewport code into platform.
|
||||||
|
// We actually want the size of the content area, in the event that we don't
|
||||||
|
// have any metadata about the width and/or height. On mobile, the screen size
|
||||||
|
// and the size of the content area are very close, or the same value.
|
||||||
|
// In XUL fennec, the content area is the size of the <browser> widget, but
|
||||||
|
// in native fennec, the content area is the size of the Gecko LayerView
|
||||||
|
// object.
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// Once bug 716575 has been resolved, this code should be changed so that it
|
||||||
|
// does the right thing on all platforms.
|
||||||
|
nsresult result;
|
||||||
|
PRInt32 screenLeft, screenTop, screenWidth, screenHeight;
|
||||||
|
nsCOMPtr<nsIScreenManager> screenMgr =
|
||||||
|
do_GetService("@mozilla.org/gfx/screenmanager;1", &result);
|
||||||
|
|
||||||
|
nsCOMPtr<nsIScreen> screen;
|
||||||
|
screenMgr->GetPrimaryScreen(getter_AddRefs(screen));
|
||||||
|
screen->GetRect(&screenLeft, &screenTop, &screenWidth, &screenHeight);
|
||||||
|
|
||||||
|
PRUint32 width = widthStr.ToInteger(&errorCode);
|
||||||
|
if (errorCode) {
|
||||||
|
if (autoSize) {
|
||||||
|
width = screenWidth;
|
||||||
|
} else {
|
||||||
|
width = Preferences::GetInt("browser.viewport.desktopWidth", 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width = NS_MIN(width, kViewportMaxWidth);
|
||||||
|
width = NS_MAX(width, kViewportMinWidth);
|
||||||
|
|
||||||
|
// Also recalculate the default zoom, if it wasn't specified in the metadata,
|
||||||
|
// and the width is specified.
|
||||||
|
if (scaleStr.IsEmpty() && !widthStr.IsEmpty()) {
|
||||||
|
scaleFloat = NS_MAX(scaleFloat, (float)(screenWidth/width));
|
||||||
|
}
|
||||||
|
|
||||||
|
PRUint32 height = heightStr.ToInteger(&errorCode);
|
||||||
|
|
||||||
|
if (errorCode) {
|
||||||
|
height = width * ((float)screenHeight / screenWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If height was provided by the user, but width wasn't, then we should
|
||||||
|
// calculate the width.
|
||||||
|
if (widthStr.IsEmpty() && !heightStr.IsEmpty()) {
|
||||||
|
width = (PRUint32) ((height * screenWidth) / screenHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
height = NS_MIN(height, kViewportMaxHeight);
|
||||||
|
height = NS_MAX(height, kViewportMinHeight);
|
||||||
|
|
||||||
|
// We need to perform a conversion, but only if the initial or maximum
|
||||||
|
// scale were set explicitly by the user.
|
||||||
|
if (!scaleStr.IsEmpty() && !scaleErrorCode) {
|
||||||
|
width = NS_MAX(width, (PRUint32)(screenWidth / scaleFloat));
|
||||||
|
height = NS_MAX(height, (PRUint32)(screenHeight / scaleFloat));
|
||||||
|
} else if (!maxScaleStr.IsEmpty() && !scaleMaxErrorCode) {
|
||||||
|
width = NS_MAX(width, (PRUint32)(screenWidth / scaleMaxFloat));
|
||||||
|
height = NS_MAX(height, (PRUint32)(screenHeight / scaleMaxFloat));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool allowZoom = true;
|
||||||
|
nsAutoString userScalable;
|
||||||
|
aDocument->GetHeaderData(nsGkAtoms::viewport_user_scalable, userScalable);
|
||||||
|
|
||||||
|
if ((userScalable.EqualsLiteral("0")) ||
|
||||||
|
(userScalable.EqualsLiteral("no")) ||
|
||||||
|
(userScalable.EqualsLiteral("false"))) {
|
||||||
|
allowZoom = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.allowZoom = allowZoom;
|
||||||
|
ret.width = width;
|
||||||
|
ret.height = height;
|
||||||
|
ret.defaultZoom = scaleFloat;
|
||||||
|
ret.minZoom = scaleMinFloat;
|
||||||
|
ret.maxZoom = scaleMaxFloat;
|
||||||
|
ret.autoSize = autoSize;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
/* static */
|
/* static */
|
||||||
nsresult
|
nsresult
|
||||||
nsContentUtils::ProcessViewportInfo(nsIDocument *aDocument,
|
nsContentUtils::ProcessViewportInfo(nsIDocument *aDocument,
|
||||||
|
|
|
@ -4804,7 +4804,18 @@ nsLayoutUtils::FontSizeInflationFor(const nsIFrame *aFrame,
|
||||||
/* static */ bool
|
/* static */ bool
|
||||||
nsLayoutUtils::FontSizeInflationEnabled(nsPresContext *aPresContext)
|
nsLayoutUtils::FontSizeInflationEnabled(nsPresContext *aPresContext)
|
||||||
{
|
{
|
||||||
return (sFontSizeInflationEmPerLine != 0 ||
|
if ((sFontSizeInflationEmPerLine == 0 &&
|
||||||
sFontSizeInflationMinTwips != 0) &&
|
sFontSizeInflationMinTwips == 0) ||
|
||||||
!aPresContext->IsChrome();
|
aPresContext->IsChrome()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewportInfo vInf =
|
||||||
|
nsContentUtils::GetViewportInfo(aPresContext->PresShell()->GetDocument());
|
||||||
|
|
||||||
|
if (vInf.defaultZoom >= 1.0 || vInf.autoSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!--
|
||||||
|
Without the patch for bug 706198, this website should not show up as inflated. That means
|
||||||
|
that with a 450px container, the minimum font size with 15em per line should be 30px.
|
||||||
|
So, we map 0px-45px into 30px-45px, and thus 12px gets mapped to 34px.
|
||||||
|
|
||||||
|
With the patch, the text should be uninflated, which means that 12px should still be
|
||||||
|
12px.
|
||||||
|
-->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content='True' name='HandheldFriendly' />
|
||||||
|
<style>
|
||||||
|
p { font-size: 12px; line-height: 1.0;}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<p>Some uninflated text.</p>
|
||||||
|
</body>
|
||||||
|
</head>
|
||||||
|
</html>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!--
|
||||||
|
Without the patch for bug 706198, this website should not show up as inflated. That means
|
||||||
|
that with a 450px container, the minimum font size with 15em per line should be 30px.
|
||||||
|
So, we map 0px-45px into 30px-45px, and thus 12px gets mapped to 34px.
|
||||||
|
|
||||||
|
With the patch, the text should be uninflated, which means that 12px should still be
|
||||||
|
12px.
|
||||||
|
-->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' name='viewport' />
|
||||||
|
<style>
|
||||||
|
p { font-size: 12px; line-height: 1.0;}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<p>Some uninflated text.</p>
|
||||||
|
</body>
|
||||||
|
</head>
|
||||||
|
</html>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!--
|
||||||
|
Without the patch for bug 706198, this website should not show up as inflated. That means
|
||||||
|
that with a 450px container, the minimum font size with 15em per line should be 30px.
|
||||||
|
So, we map 0px-45px into 30px-45px, and thus 12px gets mapped to 34px.
|
||||||
|
|
||||||
|
With the patch, the text should be uninflated, which means that 12px should still be
|
||||||
|
12px.
|
||||||
|
-->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=320"/>
|
||||||
|
<style>
|
||||||
|
p { font-size: 12px; line-height: 1.0;}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<p>Some uninflated text.</p>
|
||||||
|
</body>
|
||||||
|
</head>
|
||||||
|
</html>
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!--
|
||||||
|
Without the patch for bug 706198, this website should not show up as inflated. That means
|
||||||
|
that with a 450px container, the minimum font size with 15em per line should be 30px.
|
||||||
|
So, we map 0px-45px into 30px-45px, and thus 12px gets mapped to 34px.
|
||||||
|
|
||||||
|
With the patch, the text should be uninflated, which means that 12px should still be
|
||||||
|
12px.
|
||||||
|
-->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
p { font-size: 12px; line-height: 1.0;}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<p>Some uninflated text.</p>
|
||||||
|
</body>
|
||||||
|
</head>
|
||||||
|
</html>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Without the patch for bug 706198, this website should not show up as inflated. That means
|
||||||
|
that with a 450px container, the minimum font size with 15em per line should be 30px.
|
||||||
|
So, we map 0px-45px into 30px-45px, and thus 12px gets mapped to 34px.
|
||||||
|
|
||||||
|
With the patch, the text should be uninflated, which means that 12px should still be
|
||||||
|
12px.
|
||||||
|
-->
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
p { font-size: 12px; line-height: 1.0;}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<p>Some uninflated text.</p>
|
||||||
|
</body>
|
||||||
|
</head>
|
||||||
|
</html>
|
|
@ -61,6 +61,10 @@ var gTests = [
|
||||||
"!= select-combobox-2.html select-combobox-2.html",
|
"!= select-combobox-2.html select-combobox-2.html",
|
||||||
"!= input-checkbox.html input-checkbox.html",
|
"!= input-checkbox.html input-checkbox.html",
|
||||||
"!= input-radio.html input-radio.html",
|
"!= input-radio.html input-radio.html",
|
||||||
|
"== disable-fontinfl-on-mobile.html disable-fontinfl-on-mobile-ref.html",
|
||||||
|
"== disable-fontinfl-on-mobile-2.html disable-fontinfl-on-mobile-ref.html",
|
||||||
|
"== disable-fontinfl-on-mobile-3.html disable-fontinfl-on-mobile-ref.html",
|
||||||
|
"== disable-fontinfl-on-mobile-4.html disable-fontinfl-on-mobile-ref.html",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Maintain a reference count of how many things we're waiting for until
|
// Maintain a reference count of how many things we're waiting for until
|
||||||
|
|
Загрузка…
Ссылка в новой задаче