Open Source/webkit과 크로미움

[code reading] Media query in webkit (linkElement)

소혼 2013. 3. 6. 22:45
반응형

[media query 간략 소개]

Media query는 media의 종류에 따라 특정 CSS 스타일을 적용하고자 할 때  사용하는 CSS 스펙 가운데 하나입니다.

(http://www.w3.org/TR/css3-mediaqueries/)



media query에 대한 article : http://html.nhncorp.com/blog/42284


다음글 : Media query in webkit (style element)

그 다음글 : Media query in webkit(import/media rule)







예를 들어 아래는 대표적인 media query의 예입니다. (이 글에서 코드리딩할 방법입니다.)

### html
<link rel="stylesheet" type="text/css" media="screen" href="sans-serif.css">
<link rel="stylesheet" type="text/css" media="print" href="serif.css">

screenprint는 가장 많이 알려진 미디어의 종류(type)입니다.


또는 아래처럼 쓸 수도 있습니다.

### css
@media screen {
 * { font-family: sans-serif }
}


또 @import rule을 사용하여 아래와 같은 방법도 있습니다.

### css

@import url(example.css) screen


style element 자체에도 media query를 사용할 수 있습니다.

### html

<style media="screen and (min-width:300px)">
body { background-color:red; }
</style>


media query라는 단어가 말하듯 단순히 media의 종류를 지정하는 것 뿐 아니라 간단한 형태의 조건을 줄 수도 있습니다.

자세한 내용은 위에 링크한 스펙을 참고하시기 바랍니다.


간단히 정리하면 다음과 같습니다.

  • 여러 media type을 지정하고 싶다면 , 를 사용합니다. (screen, print)
  • media에 추가로 특정 media feature만 적용되도록 제한하고 싶다면 media feature와 media value로 이루어지는 수식들을 and로 추가할 수 있습니다.
  • only, not과 같은 제한자를 가질 수 있습니다.


예를 들면, 아래와 같습니다.

### css

<link rel="stylesheet" media="speech and (min-device-width: 800px)" href="example.css" />



[WebKit internal]

일단 위 예제 1의 <link>의 경우부터 시작하겠습니다. (1번라인을 기준으로 설명하겠습니다.)

네트워크로부터 데이터를 가져와서 link 태그를 파싱하면 HTMLLinkElement를 만들게 됩니다.

그 다음 Attribute들을 파싱하기 위해 parseAttribute를 부릅니다.


### c++; highlight: [9, 10]

void HTMLLinkElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
{
    if (name == relAttr) {
        m_relAttribute = LinkRelAttribute(value);
        process();
    } else if (name == hrefAttr) {
         // ...
    } else if (name == mediaAttr) {
        m_media = value.string().lower();
        process();
    } else if (name == disabledAttr)
         // ...
    else {
        if (name == titleAttr && m_sheet)
            m_sheet->setTitle(value);
        HTMLElement::parseAttribute(name, value);
    }
}

만약 현재 처리할 attribute가 media라면(9 line) value(screen)를 소문자로 변경하여 m_media에 넣고 process()를 호출합니다.


### c++; highlight:[10, 11, 12]

void HTMLLinkElement::process()
{
    // ...

    if (m_disabledState != Disabled && (m_relAttribute.m_isStyleSheet || (acceptIfTypeContainsTextCSS && type.contains("text/css")))
        && document()->frame() && m_url.isValid()) {

        // ...

        bool mediaQueryMatches = true;
        if (!m_media.isEmpty()) {
            RefPtr<RenderStyle> documentStyle = StyleResolver::styleForDocument(document());
            RefPtr<MediaQuerySet> media = MediaQuerySet::createAllowingDescriptionSyntax(m_media);
            MediaQueryEvaluator evaluator(document()->frame()->view()->mediaType(), document()->frame(), documentStyle.get());
            mediaQueryMatches = evaluator.eval(media.get());
        }

        // Don't hold up render tree construction and script execution on stylesheets
        // that are not needed for the rendering at the moment.
        bool blocking = mediaQueryMatches && !isAlternate();
        addPendingSheet(blocking ? Blocking : NonBlocking);

        // Load stylesheets that are not needed for the rendering immediately with low priority.
        ResourceLoadPriority priority = blocking ? ResourceLoadPriorityUnresolved : ResourceLoadPriorityVeryLow;
        CachedResourceRequest request(ResourceRequest(document()->completeURL(m_url)), charset, priority);
        request.setInitiator(this);
        m_cachedSheet = document()->cachedResourceLoader()->requestCSSStyleSheet(request);

        if (m_cachedSheet)
            m_cachedSheet->addClient(this);
        else {
            // The request may have been denied if (for example) the stylesheet is local and the document is remote.
            m_loading = false;
            removePendingSheet();
        }

       // ...

}

기본적으로 mediaQueryMatches는 true 입니다.

그러나, m_media가 있다면, FrameView의 mediaType, frame, documentStyle과 비교하여 mediaQuery가 match되는지 확인합니다.(9-12 line)

MediaQueryEvaluator가 이 값들을 가지고 있다가 eval을 통해 비교할 것입니다. (12 line)


만약 match된다면, loader에게 정상적으로 로드요청을 하겠지만, 아니면, blocking되었다가 제거될 것입니다.


m_media는 간단하게는 screen과 같이 media type만 있을 수도 있지만, "screen and (color:1), projection" 과 같이 여러 media type들과 media feature로 이루어진 수식을 포함할지도 모릅니다.


따라서 MediaQueryEvaluator가 쉽게 계산할 수 있도록 MediaQuerySet::createAllowingDescriptionSyntax을 이용하여 media query set으로 변환합니다.(10 line)


### c++ : highlight: [7, 9, 15]

bool MediaQuerySet::parse(const String& mediaString)
{
    CSSParser parser(CSSStrictMode);

    Vector<OwnPtr<MediaQuery> > result;
    Vector<String> list;
    mediaString.split(',', list);
    for (unsigned i = 0; i < list.size(); ++i) {
        String medium = list[i].stripWhiteSpace();
        if (medium.isEmpty()) {
            if (!m_fallbackToDescriptor)
                return false;
            continue;
        }
        OwnPtr<MediaQuery> mediaQuery = parser.parseMediaQuery(medium);
        if (!mediaQuery) {
            if (!m_fallbackToDescriptor)
                return false;
            String mediaDescriptor = parseMediaDescriptor(medium);
            if (mediaDescriptor.isNull())
                continue;
            mediaQuery = adoptPtr(new MediaQuery(MediaQuery::None, mediaDescriptor, nullptr));
        }
        result.append(mediaQuery.release());
    }
    // ",,,," falls straight through, but is not valid unless fallback
    if (!m_fallbackToDescriptor && list.isEmpty()) {
        String strippedMediaString = mediaString.stripWhiteSpace();
        if (!strippedMediaString.isEmpty())
            return false;
    }
    m_queries.swap(result);
    return true;
}

createAllowingDescriptionSyntax는 m_fallbackToDescriptor를 true로 하는 MediaQuerySet을 만들어 parse를 부르는게 전부입니다.

parse는 넘겨받은 mediaString을 ,로 구분해서(7 line) strip하고 (9 line) CSSParser에게 parsing을 요청하는게 전부입니다.(15 line)

MediaQuery는 세가지 주요 인자를 갖는데 Restrictor( Only, Not, None)와 mediaType(String), ExpressionVector입니다.


ExpressionVector 는 MediaQueryExp를 갖는 자료형으로 Exp가 만들어지면서 유효한 expression들이 vector에 들어가게 됩니다.


이제 MediaQuery는 만들어졌으니 이전 소스코드에서 MediaQueryEvaluator::eval을 보도록 하겠습니다.

### c++; highlight: [6, 18, 19, 24]

bool MediaQueryEvaluator::eval(const MediaQuerySet* querySet, StyleResolver* styleResolver) const
{
    if (!querySet)
        return true;

    const Vector<OwnPtr<MediaQuery> >& queries = querySet->queryVector();
    if (!queries.size())
        return true; // empty query list evaluates to true

    // iterate over queries, stop if any of them eval to true (OR semantics)
    bool result = false;
    for (size_t i = 0; i < queries.size() && !result; ++i) {
        MediaQuery* query = queries[i].get();

        if (query->ignored())
            continue;

        if (mediaTypeMatch(query->mediaType())) {
            const Vector<OwnPtr<MediaQueryExp> >* exps = query->expressions();
            // iterate through expressions, stop if any of them eval to false
            // (AND semantics)
            size_t j = 0;
            for (; j < exps->size(); ++j) {
                bool exprResult = eval(exps->at(j).get());
                if (styleResolver && exps->at(j)->isViewportDependent())
                    styleResolver->addViewportDependentMediaQueryResult(exps->at(j).get(), exprResult);
                if (!exprResult)
                    break;
            }

            // assume true if we are at the end of the list,
            // otherwise assume false
            result = applyRestrictor(query->restrictor(), exps->size() == j);
        } else
            result = applyRestrictor(query->restrictor(), false);
    }

    return result;
}

6 line에서 qureySet을 vector로 변환하여 loop를 돌리면서

먼저 type이 같은지 확인합니다.(18 line)


같다면 expression들을 꺼내 (19 line), 수식을 eval()합니다.

전 예제에서 styleResolver를 넘기지 않았기 때문에 addViewportDependentMediaQueryResult는 수행하지 않습니다.

* 현재 사용중인 예제는 media type만 갖고 있습니다. 만약에 max-width:300처럼 media expression이 있다면 어떻게 될까요?

   현재 width가 1024라고 가정하면, 해당 엘리먼트는 로드하지 않을 것입니다.

   그러나, 브라우저 크기를 줄여서 width가 300이하가 되면? 해당 스타일 시트를 로드해야 합니다.

   이러한 처리를 하기 위한 것이 바로 addViewportDependentMediaQueryResult입니다.

   이 부분의 동작은 다음 글과 다다음 글을 통해 따라가 보도록 하겠습니다.


### c++; highlight: [6, 20]

static void createFunctionMap()
{
    // Create the table.
    gFunctionMap = new FunctionMap;
#define ADD_TO_FUNCTIONMAP(name, str)  \
    gFunctionMap->set(name##MediaFeature.impl(), name##MediaFeatureEval);
    CSS_MEDIAQUERY_NAMES_FOR_EACH_MEDIAFEATURE(ADD_TO_FUNCTIONMAP);
#undef ADD_TO_FUNCTIONMAP
}

bool MediaQueryEvaluator::eval(const MediaQueryExp* expr) const
{
    if (!m_frame || !m_style)
        return m_expResult;

    if (!expr->isValid())
        return false;

    if (!gFunctionMap)
        createFunctionMap();

    // call the media feature evaluation function. Assume no prefix
    // and let trampoline functions override the prefix if prefix is
    // used
    EvalFunc func = gFunctionMap->get(expr->mediaFeature().impl());
    if (func)
        return func(expr->value(), m_style.get(), m_frame, NoPrefix);

    return false;
}

수식의 eval은 mediaFeature마다 정의된 함수를 수행하는 것입니다.

함수의 등록은 createFunctionMap안의 매크로로 이루어집니다.(20 line)


현재 등록된 mediaFeature는이 글 제일 아래를 참고하시기 바랍니다. 웹킷은 현재 CSS 스펙에 없는 media feature도 몇 가지 지원하고 있습니다.(예: device-pixel-ratio)


media feature마다 함수와 구현이 다 다르기 때문에 각각의 구현을 확인하시려면 Source/WebCore/css/MediaQueryEvaluator.cpp 를 참고하시기 바랍니다.


예제로 color를 보도록 하겠습니다. 6 line에서 보듯 'media feature명' + MediaFeatureEval()이라는 함수를 봐야 찾아야 합니다.


### c++; highlight: [3, 6]

static bool colorMediaFeatureEval(CSSValue* value, RenderStyle*, Frame* frame, MediaFeaturePrefix op)
{                                                                                      
    int bitsPerComponent = screenDepthPerComponent(frame->page()->mainFrame()->view());
    float number;                                                                      
    if (value)                                                                         
        return numberValue(value, number) && compareValue(bitsPerComponent, static_cast<int>(number), op);

    return bitsPerComponent != 0;                                                      

color 의 경우 screenDepthPerComponent라는 platform specific function으로부터 값을 꺼내오고 있습니다.해당 함수는 Source/WebCore/platform/xxx/PlatformScreenXXX.cpp에 각 포트별로 구현되어 있습니다.(XXX는 포트명)


value가 있다는 것은 현재 확인하고 있는 expression이 >, < 와 같은 비교문이 있다는 뜻(ex - color:256)입니다.

MediaFeaturePrefix는 비교하는 media feature앞에 min- 또느 max-가 붙어있는지를 전달합니다. 이 값에 따라 compareValue가 ==을 비교할지 <= 나 >=를 비교할지가 결정됩니다.





현재 등록된 media feature의 종류

### c++

#define CSS_MEDIAQUERY_NAMES_FOR_EACH_MEDIAFEATURE(macro) \
    macro(color, "color") \
    macro(grid, "grid") \
    macro(monochrome, "monochrome") \
    macro(height, "height") \
    macro(hover, "hover") \
    macro(width, "width") \
    macro(orientation, "orientation") \
    macro(aspect_ratio, "aspect-ratio") \
    macro(device_aspect_ratio, "device-aspect-ratio") \
    macro(device_pixel_ratio, "-webkit-device-pixel-ratio") \
    macro(device_height, "device-height") \
    macro(device_width, "device-width") \
    macro(max_color, "max-color") \
    macro(max_aspect_ratio, "max-aspect-ratio") \
    macro(max_device_aspect_ratio, "max-device-aspect-ratio") \
    macro(max_device_pixel_ratio, "-webkit-max-device-pixel-ratio") \
    macro(max_device_height, "max-device-height") \
    macro(max_device_width, "max-device-width") \
    macro(max_height, "max-height") \
    macro(max_monochrome, "max-monochrome") \
    macro(max_width, "max-width") \
    macro(max_resolution, "max-resolution") \
    macro(min_color, "min-color") \
    macro(min_aspect_ratio, "min-aspect-ratio") \
    macro(min_device_aspect_ratio, "min-device-aspect-ratio") \
    macro(min_device_pixel_ratio, "-webkit-min-device-pixel-ratio") \
    macro(min_device_height, "min-device-height") \
    macro(min_device_width, "min-device-width") \
    macro(min_height, "min-height") \
    macro(min_monochrome, "min-monochrome") \
    macro(min_width, "min-width") \
    macro(min_resolution, "min-resolution") \
    macro(pointer, "pointer") \
    macro(resolution, "resolution") \
    macro(transform_2d, "-webkit-transform-2d") \
    macro(transform_3d, "-webkit-transform-3d") \
    macro(transition, "-webkit-transition") \
    macro(animation, "-webkit-animation") \
    macro(view_mode, "-webkit-view-mode")



반응형